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

# Nuxt

> Build content-driven Nuxt applications with ButterCMS. Covers setup, fetching pages, collections, components, and blog 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](../../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="Nuxt Starter Project" icon="rocket" href="../starter-projects/nuxt">
  Hit the ground running with a pre-configured Nuxt + 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}
NUXT_PUBLIC_BUTTER_CMS_API_KEY=your_api_token
```

## Initialize the client

Create a composable for the ButterCMS client:

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

export function useButter() {
  const config = useRuntimeConfig();
  return Butter(config.public.butterCmsApiKey);
}
```

Update `nuxt.config.ts`:

```typescript theme={null}
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      butterCmsApiKey: process.env.NUXT_PUBLIC_BUTTER_CMS_API_KEY,
    },
  },
});
```

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

## Pages

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

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

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

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

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

  <Tab title="Single Blog Post">
    ```vue theme={null}
    <!-- pages/blog/[slug].vue -->
    <script setup lang="ts">
    const route = useRoute();
    const butter = useButter();

    interface Post {
      title: string;
      body?: string;
      published: string | null;
      featured_image: string | null;
      author: { first_name: string; last_name: string } | null;
    }

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

    if (error.value) {
      throw createError({ statusCode: 404, message: 'Post not found' });
    }

    const publishedDate = computed(() =>
      post.value?.published ? new Date(post.value.published).toLocaleDateString() : null
    );
    </script>

    <template>
      <article v-if="post">
        <h1>{{ post.title }}</h1>
        <p>
          <span v-if="post.author">By {{ post.author.first_name }} {{ post.author.last_name }}</span>
          <span v-if="publishedDate"> on {{ publishedDate }}</span>
        </p>
        <img v-if="post.featured_image" :src="post.featured_image" :alt="post.title" />
        <div v-html="post.body ?? ''" />
        <NuxtLink to="/blog">Back to Posts</NuxtLink>
      </article>
    </template>
    ```
  </Tab>
</Tabs>

## Preview Mode

Enable content editors to preview draft content:

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

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

<CardGroup cols={2}>
  <Card title="Nuxt Starter" icon="rocket" href="../starter-projects/nuxt">
    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/nuxt-starter-buttercms">
    View source code
  </Card>

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