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

# Angular

> Build content-driven Angular 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="Angular Starter Project" icon="rocket" href="../starter-projects/angular">
  Hit the ground running with a pre-configured Angular + 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 `environment.ts`:

```typescript theme={null}
// src/environments/environment.ts
export const environment = {
  production: false,
  butterCmsApiKey: 'your_api_token'
};
```

## Initialize the client

Create a reusable service:

```typescript theme={null}
// src/app/services/buttercms.service.ts
import { Injectable } from '@angular/core';
import Butter from 'buttercms';
import { environment } from '../../environments/environment';

export interface Author {
  first_name?: string;
  last_name?: string;
  slug?: string;
}

export interface Post {
  slug: string;
  title: string;
  summary: string;
  body: string;
  featured_image?: string;
  published?: string;
  author?: Author;
}

export interface Meta {
  next_page: number | null;
  previous_page: number | null;
  count: number;
}

@Injectable({
  providedIn: 'root'
})
export class ButterCMSService {
  private butter = Butter(environment.butterCmsApiKey);

  async getPage<T>(pageType: string, slug: string): Promise<T> {
    const response = await this.butter.page.retrieve(pageType, slug);
    if (!response.data?.data) {
      throw new Error('Failed to load page from ButterCMS');
    }
    return response.data.data as T;
  }

  async getPages<T>(pageType: string, params = {}): Promise<T[]> {
    const response = await this.butter.page.list(pageType, params);
    if (!response.data?.data) {
      throw new Error('Failed to load pages from ButterCMS');
    }
    return response.data.data as T[];
  }

  async getCollection<T>(keys: string[]): Promise<T> {
    const response = await this.butter.content.retrieve(keys);
    if (!response.data?.data) {
      throw new Error('Failed to load collection from ButterCMS');
    }
    return response.data.data as T;
  }

  async getPosts(params = {}): Promise<{ posts: Post[]; meta: Meta | null }> {
    const response = await this.butter.post.list(params);
    if (!response.data?.data) {
      throw new Error('Failed to load posts from ButterCMS');
    }
    return { posts: response.data.data as Post[], meta: response.data.meta ?? null };
  }

  async getPost(slug: string): Promise<Post | null> {
    const response = await this.butter.post.retrieve(slug);
    return (response.data?.data as Post) ?? null;
  }
}
```

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

## Pages

<Info>
  Angular is **zoneless** by default (standalone since v17, and the v22 CLI), so these examples
  use **signals** for reactive state — signal updates notify the zoneless change detector,
  whereas mutating a plain field (`this.loading = false`) would not re-render the view. For an
  NgModule-based app, declare these components in your module — the ButterCMS code is identical.
</Info>

```typescript theme={null}
// src/app/pages/landing/landing.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { ButterCMSService } from '../../services/buttercms.service';

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

@Component({
  selector: 'app-landing',
  standalone: true,
  imports: [CommonModule],
  template: `
    <main *ngIf="page() as p">
      <h1>{{ p.fields.headline }}</h1>
      <p>{{ p.fields.subheadline }}</p>
      <img *ngIf="p.fields.hero_image"
           [src]="p.fields.hero_image"
           [alt]="p.fields.headline" />
      <div [innerHTML]="p.fields.body"></div>
    </main>
    <div *ngIf="loading()">Loading...</div>
    <div *ngIf="error() as err">{{ err }}</div>
  `
})
export class LandingComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private butterCMS = inject(ButterCMSService);

  page = signal<{ fields: PageFields } | null>(null);
  loading = signal(true);
  error = signal<string | null>(null);

  async ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug') || 'home';
    try {
      this.page.set(await this.butterCMS.getPage<{ fields: PageFields }>('landing_page', slug));
    } catch {
      this.error.set('Page not found');
    } finally {
      this.loading.set(false);
    }
  }
}
```

## Collections

```typescript theme={null}
// src/app/pages/brands/brands.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButterCMSService } from '../../services/buttercms.service';

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

@Component({
  selector: 'app-brands',
  standalone: true,
  imports: [CommonModule],
  template: `
    <main>
      <h1>Our Brands</h1>
      <ul>
        <li *ngFor="let brand of brands()">
          <img [src]="brand.logo" [alt]="brand.name" />
          <h2>{{ brand.name }}</h2>
          <div [innerHTML]="brand.description"></div>
        </li>
      </ul>
    </main>
  `
})
export class BrandsComponent implements OnInit {
  private butterCMS = inject(ButterCMSService);
  brands = signal<Brand[]>([]);

  async ngOnInit() {
    const content = await this.butterCMS.getCollection<{ brands: Brand[] }>(['brands']);
    this.brands.set(content.brands);
  }
}
```

## Dynamic components

### Component Renderer

```typescript theme={null}
// src/app/components/component-renderer/component-renderer.component.ts
import { Component, Input, Type, ViewChild, ViewContainerRef, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeroComponent } from '../hero/hero.component';

export interface ButterComponent {
  type: string;
  fields: Record<string, unknown>;
}

// Register each block type you build. Add Features, CTA, etc. the same way
// as Hero below — create the component, then map its Butter component slug here.
const COMPONENT_MAP: Record<string, Type<object>> = {
  hero: HeroComponent,
};

@Component({
  selector: 'app-component-renderer',
  standalone: true,
  imports: [CommonModule],
  template: `<ng-container #container></ng-container>`
})
export class ComponentRendererComponent implements OnInit {
  @Input() components: ButterComponent[] = [];
  @ViewChild('container', { read: ViewContainerRef, static: true })
  container!: ViewContainerRef;

  ngOnInit() {
    this.renderComponents();
  }

  private renderComponents() {
    this.container.clear();
    for (const component of this.components) {
      const componentType = COMPONENT_MAP[component.type];
      if (componentType) {
        const componentRef = this.container.createComponent(componentType);
        Object.assign(componentRef.instance, component.fields);
      }
    }
  }
}
```

### Example Component

```typescript theme={null}
// src/app/components/hero/hero.component.ts
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-hero',
  standalone: true,
  imports: [CommonModule],
  template: `
    <section class="hero">
      <h1>{{ headline }}</h1>
      <p>{{ subheadline }}</p>
      <a *ngIf="button_label" [href]="button_url" class="btn">{{ button_label }}</a>
      <img *ngIf="image" [src]="image" [alt]="headline" />
    </section>
  `
})
export class HeroComponent {
  @Input() headline = '';
  @Input() subheadline = '';
  @Input() image = '';
  @Input() button_label = '';
  @Input() button_url = '';
}
```

### 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. Add a
  `components/:slug` route (see Routing) to view it.
</Info>

```typescript theme={null}
// src/app/pages/component-page/component-page.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { ButterCMSService } from '../../services/buttercms.service';
import {
  ButterComponent,
  ComponentRendererComponent,
} from '../../components/component-renderer/component-renderer.component';

interface ComponentPageFields {
  body: ButterComponent[];
}

@Component({
  selector: 'app-component-page',
  standalone: true,
  imports: [CommonModule, ComponentRendererComponent],
  template: `
    <main *ngIf="page() as p">
      <app-component-renderer [components]="p.fields.body"></app-component-renderer>
    </main>
    <div *ngIf="loading()">Loading...</div>
  `
})
export class ComponentPageComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private butterCMS = inject(ButterCMSService);

  page = signal<{ fields: ComponentPageFields } | null>(null);
  loading = signal(true);

  async ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug') || 'home';
    try {
      this.page.set(await this.butterCMS.getPage<{ fields: ComponentPageFields }>('component_page', slug));
    } finally {
      this.loading.set(false);
    }
  }
}
```

## Blog

<Tabs>
  <Tab title="Blog Post List">
    ```typescript theme={null}
    // src/app/pages/blog/blog-list.component.ts
    import { Component, OnInit, inject, signal } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterModule } from '@angular/router';
    import { ButterCMSService, Meta, Post } from '../../services/buttercms.service';

    @Component({
      selector: 'app-blog-list',
      standalone: true,
      imports: [CommonModule, RouterModule],
      template: `
        <main>
          <h1>Blog</h1>
          <ul>
            <li *ngFor="let post of posts()">
              <h2><a [routerLink]="['/blog', post.slug]">{{ post.title }}</a></h2>
              <p [innerHTML]="post.summary"></p>
              <span *ngIf="post.author">By {{ post.author.first_name }} {{ post.author.last_name }}</span>
            </li>
          </ul>
          <a *ngIf="meta()?.next_page"
             [routerLink]="['/blog']"
             [queryParams]="{page: meta()?.next_page}">
            Next Page
          </a>
        </main>
      `
    })
    export class BlogListComponent implements OnInit {
      private butterCMS = inject(ButterCMSService);
      posts = signal<Post[]>([]);
      meta = signal<Meta | null>(null);

      async ngOnInit() {
        const result = await this.butterCMS.getPosts({ page: 1, page_size: 10 });
        this.posts.set(result.posts);
        this.meta.set(result.meta);
      }
    }
    ```
  </Tab>

  <Tab title="Single Blog Post">
    ```typescript theme={null}
    // src/app/pages/blog/blog-post.component.ts
    import { Component, OnInit, inject, signal } from '@angular/core';
    import { CommonModule, DatePipe } from '@angular/common';
    import { ActivatedRoute, RouterModule } from '@angular/router';
    import { ButterCMSService, Post } from '../../services/buttercms.service';

    @Component({
      selector: 'app-blog-post',
      standalone: true,
      imports: [CommonModule, RouterModule, DatePipe],
      template: `
        <article *ngIf="post() as p">
          <h1>{{ p.title }}</h1>
          <p>
            <span *ngIf="p.author">By {{ p.author.first_name }} {{ p.author.last_name }}</span>
            <span *ngIf="p.published"> on {{ p.published | date:'mediumDate' }}</span>
          </p>
          <img *ngIf="p.featured_image"
               [src]="p.featured_image"
               [alt]="p.title" />
          <div [innerHTML]="p.body"></div>
          <a routerLink="/blog">Back to Posts</a>
        </article>
      `
    })
    export class BlogPostComponent implements OnInit {
      private route = inject(ActivatedRoute);
      private butterCMS = inject(ButterCMSService);
      post = signal<Post | null>(null);

      async ngOnInit() {
        const slug = this.route.snapshot.paramMap.get('slug');
        if (!slug) {
          return;
        }
        this.post.set(await this.butterCMS.getPost(slug));
      }
    }
    ```
  </Tab>
</Tabs>

## Caching

Use a caching service for better performance:

```typescript theme={null}
// src/app/services/butter-cache.service.ts
import { Injectable } from '@angular/core';
import { ButterCMSService, Meta, Post } from './buttercms.service';

interface CacheEntry<T> {
  data: T;
  timestamp: number;
}

@Injectable({
  providedIn: 'root'
})
export class ButterCacheService {
  private cache = new Map<string, CacheEntry<unknown>>();
  private ttl = 3600000; // 1 hour in milliseconds

  constructor(private butterCMS: ButterCMSService) {}

  async getPage<T>(pageType: string, slug: string): Promise<T> {
    const cacheKey = `page_${pageType}_${slug}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data as T;
    }

    const data = await this.butterCMS.getPage<T>(pageType, slug);
    this.cache.set(cacheKey, { data, timestamp: Date.now() });
    return data;
  }

  async getPosts(params = {}): Promise<{ posts: Post[]; meta: Meta | null }> {
    const cacheKey = `posts_${JSON.stringify(params)}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data as { posts: Post[]; meta: Meta | null };
    }

    const data = await this.butterCMS.getPosts(params);
    this.cache.set(cacheKey, { data, timestamp: Date.now() });
    return data;
  }

  invalidateCache(pattern?: string) {
    if (pattern) {
      for (const key of this.cache.keys()) {
        if (key.includes(pattern)) {
          this.cache.delete(key);
        }
      }
    } else {
      this.cache.clear();
    }
  }
}
```

## SEO

```typescript theme={null}
// src/app/services/seo.service.ts
import { Injectable, inject } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';

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

export interface SeoPageFields {
  seo?: SeoData;
  headline?: string;
}

export interface SeoPage {
  fields: SeoPageFields;
}

@Injectable({
  providedIn: 'root'
})
export class SeoService {
  private meta = inject(Meta);
  private title = inject(Title);

  updateFromPage(page: SeoPage) {
    const seo = page.fields.seo || {};
    const headline = page.fields.headline || '';

    this.title.setTitle(seo.title || headline);
    this.meta.updateTag({ name: 'description', content: seo.description || '' });

    // Open Graph
    this.meta.updateTag({ property: 'og:title', content: seo.og_title || seo.title || headline });
    this.meta.updateTag({ property: 'og:description', content: seo.og_description || seo.description || '' });
    if (seo.og_image) {
      this.meta.updateTag({ property: 'og:image', content: seo.og_image });
    }
  }
}
```

```typescript theme={null}
// Usage in component
import { Component, OnInit, inject } from '@angular/core';
import { ButterCMSService } from '../../services/buttercms.service';
import { SeoPage, SeoService } from '../../services/seo.service';

@Component({
  selector: 'app-landing',
  standalone: true,
  template: ''
})
export class LandingComponent implements OnInit {
  private butterCMS = inject(ButterCMSService);
  private seoService = inject(SeoService);

  async ngOnInit() {
    const page = await this.butterCMS.getPage<SeoPage>('landing_page', 'home');
    this.seoService.updateFromPage(page);
  }
}
```

## Routing

```typescript theme={null}
// src/app/app.routes.ts (Standalone)
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./pages/landing/landing.component').then(m => m.LandingComponent) },
  { path: 'page/:slug', loadComponent: () => import('./pages/landing/landing.component').then(m => m.LandingComponent) },
  { path: 'brands', loadComponent: () => import('./pages/brands/brands.component').then(m => m.BrandsComponent) },
  { path: 'blog', loadComponent: () => import('./pages/blog/blog-list.component').then(m => m.BlogListComponent) },
  { path: 'blog/:slug', loadComponent: () => import('./pages/blog/blog-post.component').then(m => m.BlogPostComponent) },
  { path: 'components/:slug', loadComponent: () => import('./pages/component-page/component-page.component').then(m => m.ComponentPageComponent) },
];
```

## Resources

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

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