Setting up a blog with Vue and Sanity: Part 2
Published at 7/29/2022, 11:22:48 AM
In the previous blog post we began to create a blog with Vue and Sanity. First we configured Sanity and then created the first schemas for the blog content. Next we still need to…
- Initialize Sanity client
- Add a view and components for the website to render the blog content
- Fetch the content with Sanity’s GROQ query language (Graph-Relational Object Queries)
Note: As I’m creating this blog section to my own website I’m going to assume that a basic Vue application with Vue Router is already defined so we will only add routing and components for the blog and add minimal styling. I love using TypeScript so the guide will also use the Vue 3 Composition API which has a superior TypeScript support to previous versions.
Let’s begin.
Initializing Sanity client
In the previous guide, as we were setting up Sanity Studio, we installed everything inside the /studio folder but this time everything we will do will pertain to the application itself. So just follow your own folder structure. Most likely you will do everything in the /src folder.
First let’s install the Sanity client and a couple useful packages for handling the data provided by Sanity.
npm install @sanity/client @sanity/image-url sanity-blocks-vue-component
So here’s what we installed.
- @sanity/client is responsible for fetching and handling the data from Sanity
- @sanity/image-url generates image URLs from Sanity’s custom image records
- sanity-blocks-vue-component is used to format and render the rich text data we create with our Content Block editor
Next we can setup the Sanity Client by creating client.ts inside the /src folder.
client.ts
import sanityClient from "@sanity/client";
export default sanityClient({
projectId: PROJECT_ID, apiVersion: API_VERSION,
dataset: DATASET,
useCdn: true,
});
The purpose of the sanityClient function is to generate the API URL and access the Sanity API in a simplified way. The API URL that is generated uses the following format:
https://<PROJECT_ID>.api.sanity.io/v<API_VERSION>/data/query/<DATASET>?query=<QUERY>
The PROJECT_ID is a string of your own project id which you can find in the Sanity dashboard.
The API_VERSION is an ISO date string (following the format YYYY-MM-DD) of the API version used. You can set the current date to use the latest version. At the time of writing, the latest version is “2021-10-21”.
The DATASET is the a string name of the dataset you use from your selected project. By default this is “production”.
The PROJECT_ID or DATASET are not private values, so you can assume they can be exposed to the world. Access to data is managed with CORS origins in the Sanity dashboard by selecting your project and navigating to API > CORS origins. When you add a new CORS origin, you will be able to select whether you want to allow sending authenticated requests with a user’s token or session. By default this should only be allowed for your Sanity Studio source (which should already be in the list). You don’t need to create new or edit existing blog posts from anywhere else.
So let’s say your local Vue application runs on http://localhost:8080. Then you should create a new CORS origin with the “Origin” value http://localhost:8080 and “Allow credentials” not checked. Now your application should be receiving data from your Sanity project. But before we can see it, we need to create the queries and the components.
Adding Vue components and fetching data
Let’s begin by adding routes for the blog and blog post components. If you do not have a Vue Router defined, please check the official documentation before continuing.
routes.ts
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/blog",
name: "Blog",
component: Blog,
},
{
path: "/blog/:slug",
name: "BlogPost",
component: BlogPost,
},
];
The slug for a single blog post is included in the blog post data when it is created in Sanity Studio, so as we navigate to a certain blog post, we must pass that slug to the router link.
Next we can define the Vue components. Let’s start with the blog.
views/Blog.vue
<template>
<div class="blog">
<router-link to="/">{{ "go back" }}</router-link>
<h1>Blog</h1>
<div class="posts">
<div class="loading" v-if="loading">Loading...</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div class="post-container">
<div v-for="post in posts" class="post-item" :key="post._id">
<h2>
<router-link :to="`/blog/${post.slug.current}`">
{{ post.title }}
</router-link>
</h2>
<span>
Published at {{ post.publishedAt }}
</span>
<hr />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
import sanity from "../client";
const loading = ref(false);
const error: Ref<string | null> = ref(null);
const posts: Ref<any[]> = ref([]);
const query = `*[_type == "post"]{
_id,
title,
slug,
publishedAt
}[0...25]`;
function fetchData() {
loading.value = true;
sanity.fetch(query).then(
(data) => {
loading.value = false;
posts.value = data;
},
(err) => {
error.value = JSON.stringify(err);
loading.value = false;
}
);
}
fetchData();
</script>
<style scoped>
/* Insert styles here */
</style>
For brevity’s sake I didn’t include any style definitions. Also, for the same reason, the type of the post object is any. I leave these as an exercise for the reader. However, let’s go though the code we just wrote.
The Vue component is very a simple UI that includes…
- back button
- title
- loading state handler
- error state handler
- container where any found posts are mapped with v-for
and the code in the script tag includes…
- states for loading, error and list of posts
- GROQ query for fetching posts
- function for fetching the data from Sanity
Next let’s create a component for a single blog post.
components/BlogPost.vue
<template>
<div class="blog-post">
<router-link to="/blog" style="align-self: flex-start">{{
"go back"
}}</router-link>
<div class="loading" v-if="loading">Loading...</div>
<div v-if="error" class="error">
<!-- {{ error }} -->
There doesn't seem to be anything here…
</div>
<div v-if="post" class="content">
<h1>{{ post.title }}</h1>
<img v-if="post.image" :src="imageUrlFor(post.image).width(480).url()" />
<h6>Published at {{ post.publishedAt }}</h6>
<SanityBlocks :blocks="blocks" :serializers="serializers" />
</div>
</div>
</template>
<script setup lang="ts">
import imageUrlBuilder from "@sanity/image-url";
import { SanityBlocks } from "sanity-blocks-vue-component";
import SshPre from "simple-syntax-highlighter";
import "simple-syntax-highlighter/dist/sshpre.css";
import { defineComponent, h, ref } from "vue";
import type { Ref } from "vue";
import { useRoute } from "vue-router";
import sanity from "../client";
const loading = ref(false);
const error: Ref<string | null> = ref(null);
const post: Ref<any | null> = ref(null);
const blocks = ref([]);
const query = `*[slug.current == $slug] {
_id,
title,
slug,
publishedAt,
body,
"image": mainImage{
asset->{
_id,
url
}
}
}[0]
`;
const imageBuilder = imageUrlBuilder(sanity);
function imageUrlFor(source: any) {
return imageBuilder.image(source);
}
const serializers = {
types: {
code: defineComponent({
props: ["code"],
setup(props) {
return () => h(SshPre, { dark: true }, () => [props.code]);
},
}),
image: defineComponent({
props: ["asset"],
setup(props) {
return () =>
h("img", {
src: imageUrlFor(props.asset).width(480).url(),
});
},
}),
},
} as any;
function fetchData() {
const route = useRoute();
loading.value = true;
sanity.fetch(query, { slug: route.params.slug }).then(
(data) => {
loading.value = false;
post.value = data;
blocks.value = data.body;
},
(err) => {
console.log(err);
error.value = `${err.name}: ${err.message}`;
loading.value = false;
}
);
}
fetchData();
</script>
<style scoped>
/* Insert styles here */
</style>
Again, let’s go through the code. The blog post component includes…
- back button
- loading state handler
- error state handler
- title
- image
- published date (ISO string)
- the block content (i.e. the rich text)
and the code in the script tag includes…
- states for loading, error, a single blog post and blocks (block content, i.e. the rich text)
- GROQ query for the blog post based on the given slug
- function for building url for image
- custom serializers for a couple blocks that cannot be automatically serialized with sanity-blocks-vue-component
- function for fetching the data from Sanity
Block content
The block content represents the rich text of the blog post, i.e. the main body. It is formatted in a specific way in Sanity so it needs to be formatted again for UI consumption.
For the block content we use the sanity-blocks-vue-component. This is a Vue 3 component for rendering block content from Sanity. For the most part it works right out of the box, but we do have to make a couple custom serializers for it. Namely for the code tag and the img tag. In the code above the custom elements for these are created using Vue’s render function syntax.
For the serialization of code blocks we will use the SshPre component from simple-syntax-highlighter.
For the serialization of asset blocks (images) we will use an HTML image tag. For this image tag we will generate a src url with the Sanity imageUrlBuilder function. The width for this image is set to 480 pixels.
Now that we have the view components, and even the queries, let's have a brief overview of GROQ and what the query string actually means.
GROQ
GROQ stands for Graph-Relational Object Queries. It is a query language specifically for JSON that was created by the Sanity team and is now developed as open-source software. Personally, I find it quite familiar after having used a lot of GraphQL in the past.
The query uses the following format:
*[ <filter> ]{ <projection> }
- * returns all documents in the data that the current user has permission to read
- [] encases the filter which, when evaluated to true, will return all matching documents
- {} encases the projection which determines how the result should be formatted
So looking at our blog query before...
const query = `*[_type == "post"]{
_id,
title,
slug,
publishedAt
}[0...25]`;
We can read it as “find me all documents that I have the permission to read, that have a _type value that equals 'post', and only return me values for the properties id, title, slug and publishedAt”. The final part is a range which you can use to limit the size of the array that is returned with the query.
With this brief introduction to GROQ, I will end this tutorial. There are many other things you can do with GROQ to control the data you are fetching, but I will not delve deeper into that this time.
I hope you found this guide helpful. If you have been following this guide and encounter any issues or if you have any other questions, please let me know on Twitter.