> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Vue

> Build content-driven Vue apps with ButterCMS. Fetch pages, collections, components, and blog content using Vue 3 Composition API.

## 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](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

### 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.

<Card title="Vue Starter Project" icon="rocket" href="../starter-projects/vue">
  Hit the ground running with a pre-configured Vue + ButterCMS setup.
</Card>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install buttercms
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add buttercms
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add buttercms
    ```
  </Tab>
</Tabs>

Add your API token to `.env`:

```bash theme={null}
VITE_BUTTER_CMS_API_KEY=your_api_token
```

## Initialize the client

Create a reusable client instance:

```typescript theme={null}
// src/lib/buttercms.ts
import Butter from 'buttercms';

const butter = Butter(import.meta.env.VITE_BUTTER_CMS_API_KEY);

export default butter;
```

<Info>
  For complete SDK documentation including all available methods and configuration options, see the [JavaScript SDK Reference](../sdks/javascript-sdk).
</Info>

## Pages

<Tabs>
  <Tab title="Vue Router">
    ```vue theme={null}
    <!-- 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:

    ```typescript theme={null}
    // 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;
    ```
  </Tab>

  <Tab title="Composable">
    ```typescript theme={null}
    // src/composables/usePage.ts
    import { ref, watch, type Ref } from 'vue';
    import butter from '@/lib/buttercms';

    export function usePage<T extends object>(pageType: string, slug: Ref<string>) {
      const page = ref<{ fields: 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<T>(pageType, slugValue);
          page.value = response.data?.data ?? null;
        } catch (err) {
          error.value = 'Page not found';
        } finally {
          loading.value = false;
        }
      }

      watch(slug, fetchPage, { immediate: true });

      return { page, loading, error };
    }
    ```

    Usage:

    ```vue theme={null}
    <!-- src/views/LandingPage.vue -->
    <script setup lang="ts">
    import { computed } from 'vue';
    import { useRoute } from 'vue-router';
    import { usePage } from '@/composables/usePage';

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

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

## Collections

```vue theme={null}
<!-- 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

```vue theme={null}
<!-- 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

```vue theme={null}
<!-- 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

<Info>
  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.
</Info>

```vue theme={null}
<!-- 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

<Tabs>
  <Tab title="Blog Post List">
    ```vue theme={null}
    <!-- 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>
    ```
  </Tab>

  <Tab title="Single Blog Post">
    ```vue theme={null}
    <!-- 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 | null;
      featured_image: string | null;
      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 ?? null;
      } 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 }}
          <template v-if="post.published"> on {{ new Date(post.published).toLocaleDateString() }}</template>
        </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>
    ```
  </Tab>
</Tabs>

## SEO with @vueuse/head

```vue theme={null}
<!-- 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

<CardGroup cols={2}>
  <Card title="Vue Starter" icon="rocket" href="../starter-projects/vue">
    Pre-configured starter project
  </Card>

  <Card title="JavaScript SDK" icon="js" href="../sdks/javascript-sdk">
    Complete SDK reference
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/ButterCMS/vue-starter-buttercms">
    View source code
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
