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.

Nuxt Starter Project

Hit the ground running with a pre-configured Nuxt + ButterCMS setup.

Installation

npm install buttercms
Add your API token to .env:
NUXT_PUBLIC_BUTTER_CMS_API_KEY=your_api_token

Initialize the client

Create a composable for the ButterCMS client:
// composables/useButter.ts
import Butter from 'buttercms';

export function useButter() {
  const config = useRuntimeConfig();
  return Butter(config.public.butterCmsApiKey);
}
Update nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      butterCmsApiKey: process.env.NUXT_PUBLIC_BUTTER_CMS_API_KEY,
    },
  },
});
For complete SDK documentation including all available methods and configuration options, see the JavaScript SDK Reference.

Pages

<!-- pages/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const butter = useButter();

interface PageFields {
  headline: string;
  subheadline: string;
  hero_image: string;
  body: string;
}

const { data: page, error } = await useAsyncData(
  `page-${route.params.slug}`,
  async () => {
    const response = await butter.page.retrieve<PageFields>('landing_page', route.params.slug as string);
    if (!response.data?.data) {
      throw createError({ statusCode: 502, statusMessage: 'Failed to load page from ButterCMS' });
    }
    return response.data.data;
  }
);

if (error.value) {
  throw createError({ statusCode: 404, message: 'Page not found' });
}
</script>

<template>
  <main v-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>

Collections

<!-- pages/brands.vue -->
<script setup lang="ts">
const butter = useButter();

interface Brand {
  name: string;
  logo: string;
  description: string;
}

const { data: brands } = await useAsyncData('brands', async () => {
  const response = await butter.content.retrieve(['brands']);
  if (!response.data?.data) {
    throw createError({ statusCode: 502, statusMessage: 'Failed to load brands from ButterCMS' });
  }
  return response.data.data.brands as Brand[];
});
</script>

<template>
  <main>
    <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

<!-- components/ComponentRenderer.vue -->
<script setup lang="ts">
import { defineAsyncComponent, type Component as VueComponent } from 'vue';

interface HeroComponent {
  type: 'hero';
  fields: {
    headline: string;
    subheadline: string;
    image: string;
    button_label: string;
    button_url: string;
  };
}

interface FeaturesComponent {
  type: 'features';
  fields: {
    headline: string;
    features: { title: string; description: string; icon: string }[];
  };
}

interface TestimonialsComponent {
  type: 'testimonials';
  fields: {
    headline: string;
    testimonials: { quote: string; author: string; role: string }[];
  };
}

interface CtaComponent {
  type: 'cta';
  fields: {
    headline: string;
    button_label: string;
    button_url: string;
  };
}

export type ButterComponent =
  | HeroComponent
  | FeaturesComponent
  | TestimonialsComponent
  | CtaComponent;

defineProps<{
  components: ButterComponent[];
}>();

const resolveComponent = (type: ButterComponent['type']): VueComponent | null => {
  switch (type) {
    case 'hero':
      return defineAsyncComponent(() => import('./Hero.vue'));
    case 'features':
      return defineAsyncComponent(() => import('./Features.vue'));
    case 'testimonials':
      return defineAsyncComponent(() => import('./Testimonials.vue'));
    case 'cta':
      return defineAsyncComponent(() => import('./CTA.vue'));
    default:
      return null;
  }
};
</script>

<template>
  <template v-for="(component, index) in components" :key="index">
    <component
      v-if="resolveComponent(component.type)"
      :is="resolveComponent(component.type)"
      v-bind="component.fields"
    />
  </template>
</template>

Example Component

<!-- 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.
<!-- pages/landing/[slug].vue -->
<script setup lang="ts">
import type { ButterComponent } from '~/components/ComponentRenderer.vue';

const route = useRoute();
const butter = useButter();

interface PageFields {
  body: ButterComponent[];
}

const { data: page, error } = await useAsyncData(
  `landing-${route.params.slug}`,
  async () => {
    const response = await butter.page.retrieve<PageFields>('component_page', route.params.slug as string);
    if (!response.data?.data) {
      throw createError({ statusCode: 502, statusMessage: 'Failed to load page from ButterCMS' });
    }
    return response.data.data;
  }
);

if (error.value) {
  throw createError({ statusCode: 404, message: 'Page not found' });
}
</script>

<template>
  <main v-if="page">
    <ComponentRenderer :components="page.fields.body" />
  </main>
</template>

Blog

<!-- pages/blog/index.vue -->
<script setup lang="ts">
const route = useRoute();
const butter = useButter();

interface Post {
  slug: string;
  title: string;
  summary: string;
  author: { first_name: string; last_name: string } | null;
}

const page = parseInt(route.query.page as string) || 1;

const { data } = await useAsyncData(`blog-page-${page}`, async () => {
  const response = await butter.post.list({ page, page_size: 10 });
  if (!response.data?.data) {
    throw createError({ statusCode: 502, statusMessage: 'Failed to load posts from ButterCMS' });
  }
  return {
    posts: response.data.data as Post[],
    meta: response.data.meta,
  };
});
</script>

<template>
  <main v-if="data">
    <h1>Blog</h1>
    <ul>
      <li v-for="post in data.posts" :key="post.slug">
        <h2><NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink></h2>
        <p v-html="post.summary ?? ''" />
        <span v-if="post.author">By {{ post.author.first_name }} {{ post.author.last_name }}</span>
      </li>
    </ul>
    <NuxtLink v-if="data.meta?.next_page" :to="`/blog?page=${data.meta.next_page}`">
      Next Page
    </NuxtLink>
  </main>
</template>

Preview Mode

Enable content editors to preview draft content:
// composables/useButter.ts
import Butter from 'buttercms';

export function useButter() {
  const config = useRuntimeConfig();
  const route = useRoute();
  const isPreview = route.query.preview === 'true';

  return Butter(config.public.butterCmsApiKey, isPreview);
}

SEO

<!-- pages/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const butter = useButter();

interface Seo {
  title?: string;
  description?: string;
  og_title?: string;
  og_description?: string;
  og_image?: string;
}

interface PageFields {
  headline: string;
  seo?: Seo;
}

const { data: page } = await useAsyncData(
  `page-${route.params.slug}`,
  async () => {
    const response = await butter.page.retrieve<PageFields>('landing_page', route.params.slug as string);
    if (!response.data?.data) {
      throw createError({ statusCode: 502, statusMessage: 'Failed to load page from ButterCMS' });
    }
    return response.data.data;
  }
);

const seo = computed<Seo>(() => page.value?.fields.seo ?? {});

useHead({
  title: () => seo.value.title || page.value?.fields.headline,
  meta: [
    { name: 'description', content: () => seo.value.description },
    { property: 'og:title', content: () => seo.value.og_title || seo.value.title },
    { property: 'og:description', content: () => seo.value.og_description || seo.value.description },
    { property: 'og:image', content: () => seo.value.og_image },
  ],
});
</script>

Resources

Nuxt Starter

Pre-configured starter project

JavaScript SDK

Complete SDK reference

GitHub Repository

View source code

Content API

REST API documentation