Overview
This integration guide shows you how to how to update your existing project to:- install the ButterCMS package
- instantiate ButterCMS
- create components to fetch and display each of the three ButterCMS content types: Pages, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.
Starter project
Or, you can jump directly to the starter project below, which will allow you to clone, install, run, and deploy a fully working starter project that’s integrated with content already inside of your ButterCMS account.Vue Starter Project
Hit the ground running with a pre-configured Vue + ButterCMS setup.
Installation
- npm
- yarn
- pnpm
npm install buttercms
yarn add buttercms
pnpm add buttercms
.env:
VITE_BUTTER_CMS_API_KEY=your_api_token
Initialize the client
Create a reusable client instance:// src/lib/buttercms.ts
import Butter from 'buttercms';
const butter = Butter(import.meta.env.VITE_BUTTER_CMS_API_KEY);
export default butter;
For complete SDK documentation including all available methods and configuration options, see the JavaScript SDK Reference.
Pages
- Vue Router
- Composable
<!-- src/views/LandingPage.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import butter from '@/lib/buttercms';
interface PageFields {
headline: string;
subheadline: string;
hero_image: string;
body: string;
}
const route = useRoute();
const page = ref<{ fields: PageFields } | null>(null);
const loading = ref(true);
const error = ref<string | null>(null);
async function fetchPage(slug: string) {
loading.value = true;
error.value = null;
try {
const response = await butter.page.retrieve('landing-page', slug);
page.value = response.data.data;
} catch (err) {
error.value = 'Page not found';
} finally {
loading.value = false;
}
}
onMounted(() => fetchPage(route.params.slug as string));
watch(() => route.params.slug, (slug) => fetchPage(slug as string));
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<main v-else-if="page">
<h1>{{ page.fields.headline }}</h1>
<p>{{ page.fields.subheadline }}</p>
<img v-if="page.fields.hero_image" :src="page.fields.hero_image" :alt="page.fields.headline" />
<div v-html="page.fields.body" />
</main>
</template>
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import LandingPage from '@/views/LandingPage.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/:slug', component: LandingPage },
],
});
export default router;
// src/composables/usePage.ts
import { ref, watch, type Ref } from 'vue';
import butter from '@/lib/buttercms';
export function usePage<T>(pageType: string, slug: Ref<string>) {
const page = ref<T | null>(null);
const loading = ref(true);
const error = ref<string | null>(null);
async function fetchPage(slugValue: string) {
loading.value = true;
error.value = null;
try {
const response = await butter.page.retrieve(pageType, slugValue);
page.value = response.data.data;
} catch (err) {
error.value = 'Page not found';
} finally {
loading.value = false;
}
}
watch(slug, fetchPage, { immediate: true });
return { page, loading, error };
}
<!-- src/views/LandingPage.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { usePage } from '@/composables/usePage';
const route = useRoute();
const slug = computed(() => route.params.slug as string);
const { page, loading, error } = usePage('landing-page', slug);
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<main v-else-if="page">
<h1>{{ page.fields.headline }}</h1>
<div v-html="page.fields.body" />
</main>
</template>
Collections
<!-- src/views/BrandsPage.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import butter from '@/lib/buttercms';
interface Brand {
name: string;
logo: string;
description: string;
}
const brands = ref<Brand[]>([]);
const loading = ref(true);
onMounted(async () => {
try {
const response = await butter.content.retrieve(['brands']);
brands.value = response.data.data.brands;
} finally {
loading.value = false;
}
});
</script>
<template>
<div v-if="loading">Loading...</div>
<main v-else>
<h1>Our Brands</h1>
<ul>
<li v-for="(brand, index) in brands" :key="index">
<img :src="brand.logo" :alt="brand.name" />
<h2>{{ brand.name }}</h2>
<div v-html="brand.description" />
</li>
</ul>
</main>
</template>
Dynamic components
Component Renderer
<!-- src/components/ComponentRenderer.vue -->
<script setup lang="ts">
import { defineAsyncComponent, type Component as VueComponent } from 'vue';
interface ButterComponent {
type: string;
fields: Record<string, any>;
}
defineProps<{
components: ButterComponent[];
}>();
const componentMap: Record<string, VueComponent> = {
hero: defineAsyncComponent(() => import('./Hero.vue')),
features: defineAsyncComponent(() => import('./Features.vue')),
testimonials: defineAsyncComponent(() => import('./Testimonials.vue')),
cta: defineAsyncComponent(() => import('./CTA.vue')),
};
</script>
<template>
<template v-for="(component, index) in components" :key="index">
<component
v-if="componentMap[component.type]"
:is="componentMap[component.type]"
v-bind="component.fields"
/>
</template>
</template>
Example Component
<!-- src/components/Hero.vue -->
<script setup lang="ts">
defineProps<{
headline: string;
subheadline: string;
image: string;
button_label: string;
button_url: string;
}>();
</script>
<template>
<section class="hero">
<h1>{{ headline }}</h1>
<p>{{ subheadline }}</p>
<a v-if="button_label" :href="button_url">{{ button_label }}</a>
<img v-if="image" :src="image" :alt="headline" />
</section>
</template>
Using in Pages
<!-- src/views/LandingPage.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { usePage } from '@/composables/usePage';
import ComponentRenderer from '@/components/ComponentRenderer.vue';
const route = useRoute();
const slug = computed(() => route.params.slug as string);
const { page, loading, error } = usePage('landing-page', slug);
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<main v-else-if="page">
<ComponentRenderer :components="page.fields.body" />
</main>
</template>
Blog
- Blog Post List
- Single Blog Post
<!-- src/views/BlogList.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import butter from '@/lib/buttercms';
interface Post {
slug: string;
title: string;
summary: string;
author: { first_name: string; last_name: string };
}
const route = useRoute();
const posts = ref<Post[]>([]);
const nextPage = ref<number | null>(null);
const loading = ref(true);
async function fetchPosts(page: number) {
loading.value = true;
try {
const response = await butter.post.list({ page, page_size: 10 });
posts.value = response.data.data;
nextPage.value = response.data.meta.next_page;
} finally {
loading.value = false;
}
}
const currentPage = () => parseInt(route.query.page as string) || 1;
onMounted(() => fetchPosts(currentPage()));
watch(() => route.query.page, () => fetchPosts(currentPage()));
</script>
<template>
<div v-if="loading">Loading...</div>
<main v-else>
<h1>Blog</h1>
<ul>
<li v-for="post in posts" :key="post.slug">
<h2><RouterLink :to="`/blog/${post.slug}`">{{ post.title }}</RouterLink></h2>
<p v-html="post.summary" />
<span>By {{ post.author.first_name }} {{ post.author.last_name }}</span>
</li>
</ul>
<RouterLink v-if="nextPage" :to="`/blog?page=${nextPage}`">Next Page</RouterLink>
</main>
</template>
<!-- src/views/BlogPost.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import butter from '@/lib/buttercms';
interface Post {
title: string;
body: string;
published: string;
featured_image: string;
author: { first_name: string; last_name: string };
}
const route = useRoute();
const post = ref<Post | null>(null);
const loading = ref(true);
const error = ref<string | null>(null);
async function fetchPost(slug: string) {
loading.value = true;
error.value = null;
try {
const response = await butter.post.retrieve(slug);
post.value = response.data.data;
} catch {
error.value = 'Post not found';
} finally {
loading.value = false;
}
}
onMounted(() => fetchPost(route.params.slug as string));
watch(() => route.params.slug, (slug) => fetchPost(slug as string));
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<article v-else-if="post">
<h1>{{ post.title }}</h1>
<p>
By {{ post.author.first_name }} {{ post.author.last_name }} on
{{ new Date(post.published).toLocaleDateString() }}
</p>
<img v-if="post.featured_image" :src="post.featured_image" :alt="post.title" />
<div v-html="post.body" />
<RouterLink to="/blog">Back to Posts</RouterLink>
</article>
</template>
SEO with vue-meta
<!-- src/views/LandingPage.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useHead } from '@vueuse/head';
import { usePage } from '@/composables/usePage';
const route = useRoute();
const slug = computed(() => route.params.slug as string);
const { page } = usePage('landing-page', slug);
useHead(computed(() => {
if (!page.value) return {};
const seo = page.value.fields.seo || {};
return {
title: seo.title || page.value.fields.headline,
meta: [
{ name: 'description', content: seo.description },
{ property: 'og:title', content: seo.og_title || seo.title },
{ property: 'og:description', content: seo.og_description || seo.description },
{ property: 'og:image', content: seo.og_image },
].filter(m => m.content),
};
}));
</script>
<template>
<main v-if="page">
<h1>{{ page.fields.headline }}</h1>
<div v-html="page.fields.body" />
</main>
</template>
Resources
Vue Starter
Pre-configured starter project
JavaScript SDK
Complete SDK reference
GitHub Repository
View source code
Content API
REST API documentation