Skip to main content

Overview

This integration guide shows you how to how to update your existing project to:
  1. install the ButterCMS package
  2. instantiate ButterCMS
  3. 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 install buttercms
Add your API token to .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

<!-- 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<PageFields>('landing_page', slug);
    page.value = response.data?.data ?? null;
  } 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>
Router setup:
// 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;

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']);
    if (!response.data?.data) {
      throw new Error('Failed to load brands from ButterCMS');
    }
    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, unknown>;
}

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

This uses a distinct page type (component_page) whose body is a Component Picker (Page Builder) field — an array of components — separate from the WYSIWYG landing_page in the Pages section (whose body is a string). A page’s body is one field type or the other, so the Page Builder example needs its own page type.
<!-- 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';

interface PageFields {
  body: Array<{ type: string; fields: Record<string, unknown> }>;
}

const route = useRoute();
const slug = computed(() => route.params.slug as string);
const { page, loading, error } = usePage<PageFields>('component_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

<!-- 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 });
    if (!response.data?.data) {
      throw new Error('Failed to load blog posts from ButterCMS');
    }
    posts.value = response.data.data;
    nextPage.value = response.data.meta?.next_page ?? null;
  } 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>

SEO with @vueuse/head

<!-- 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';

interface PageFields {
  headline: string;
  body: string;
  seo?: {
    title?: string;
    description?: string;
    og_title?: string;
    og_description?: string;
    og_image?: string;
  };
}

const route = useRoute();
const slug = computed(() => route.params.slug as string);
const { page } = usePage<PageFields>('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