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

# Ionic

> Build content-driven Ionic 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>

## 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,
  butterCmsApiToken: 'your_api_token',
};
```

## Initialize the client

Create an Angular service to wrap the ButterCMS SDK:

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

@Injectable({
  providedIn: 'root',
})
export class ButterService {
  private butter = Butter(environment.butterCmsApiToken);

  getPage<Fields extends object>(pageType: string, slug: string, params?: object) {
    return this.butter.page.retrieve<Fields>(pageType, slug, params);
  }

  getContent(keys: string[], params?: object) {
    return this.butter.content.retrieve(keys, params);
  }

  getPosts(params?: object) {
    return this.butter.post.list(params);
  }

  getPost(slug: string) {
    return this.butter.post.retrieve(slug);
  }
}
```

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

## Pages

```typescript theme={null}
// src/app/pages/landing/landing.page.ts
import { Component, OnInit, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButterService } from '../../services/butter.service';

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

@Component({
  selector: 'app-landing',
  standalone: false,
  templateUrl: './landing.page.html',
})
export class LandingPage implements OnInit {
  page = signal<{ fields: PageFields } | null>(null);
  loading = signal(true);
  error = signal<string | null>(null);

  constructor(
    private route: ActivatedRoute,
    private butter: ButterService
  ) {}

  async ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug') || 'home';
    try {
      const response = await this.butter.getPage<PageFields>('landing_page', slug);
      if (!response.data?.data) {
        throw new Error('Failed to load page from ButterCMS');
      }
      this.page.set(response.data.data);
    } catch {
      this.error.set('Page not found');
    } finally {
      this.loading.set(false);
    }
  }
}
```

```html theme={null}
<!-- src/app/pages/landing/landing.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>{{ page()?.fields?.headline }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="loading()" class="ion-text-center ion-padding">
    <ion-spinner></ion-spinner>
  </div>

  <div *ngIf="error() as err" class="ion-padding">
    <p>{{ err }}</p>
  </div>

  <div *ngIf="page() as p" class="ion-padding">
    <ng-container *ngIf="!loading()">
      <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>
    </ng-container>
  </div>
</ion-content>
```

Register the route in your app module:

```typescript theme={null}
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'page/:slug',
    loadChildren: () =>
      import('./pages/landing/landing.module').then((m) => m.LandingPageModule),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

## Collections

```typescript theme={null}
// src/app/pages/brands/brands.page.ts
import { Component, OnInit, signal } from '@angular/core';
import { ButterService } from '../../services/butter.service';

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

@Component({
  selector: 'app-brands',
  standalone: false,
  templateUrl: './brands.page.html',
})
export class BrandsPage implements OnInit {
  brands = signal<Brand[]>([]);
  loading = signal(true);

  constructor(private butter: ButterService) {}

  async ngOnInit() {
    try {
      const response = await this.butter.getContent(['brands']);
      if (!response.data?.data) {
        throw new Error('Failed to load brands from ButterCMS');
      }
      this.brands.set(response.data.data['brands'] as unknown as Brand[]);
    } finally {
      this.loading.set(false);
    }
  }
}
```

```html theme={null}
<!-- src/app/pages/brands/brands.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>Our Brands</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="loading()" class="ion-text-center ion-padding">
    <ion-spinner></ion-spinner>
  </div>

  <ion-list *ngIf="!loading()">
    <ion-item *ngFor="let brand of brands()">
      <ion-thumbnail slot="start">
        <img [src]="brand.logo" [alt]="brand.name" />
      </ion-thumbnail>
      <ion-label>
        <h2>{{ brand.name }}</h2>
        <p [innerHTML]="brand.description"></p>
      </ion-label>
    </ion-item>
  </ion-list>
</ion-content>
```

## Dynamic components

### Component registry

```typescript theme={null}
// src/app/components/component-renderer/component-renderer.component.ts
import {
  Component,
  Input,
  ComponentRef,
  Type,
  ViewChild,
  ViewContainerRef,
  OnChanges,
  SimpleChanges,
} from '@angular/core';
import { HeroComponent } from '../hero/hero.component';
import { FeaturesComponent } from '../features/features.component';
import { CtaComponent } from '../cta/cta.component';

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

const componentMap: Record<string, Type<unknown>> = {
  hero: HeroComponent,
  features: FeaturesComponent,
  cta: CtaComponent,
};

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

  ngOnChanges(changes: SimpleChanges) {
    if (changes['components']) {
      this.renderComponents();
    }
  }

  private renderComponents() {
    this.container.clear();
    this.components.forEach((item) => {
      const componentClass = componentMap[item.type];
      if (!componentClass) return;
      const ref: ComponentRef<unknown> = this.container.createComponent(componentClass);
      Object.assign(ref.instance as object, item.fields);
    });
  }
}
```

### Example Component

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

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

## Blog

<Tabs>
  <Tab title="Blog Post List">
    ```typescript theme={null}
    // src/app/pages/blog/blog.page.ts
    import { Component, OnInit, signal } from '@angular/core';
    import { ButterService } from '../../services/butter.service';

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

    @Component({
      selector: 'app-blog',
      standalone: false,
      templateUrl: './blog.page.html',
    })
    export class BlogPage implements OnInit {
      posts = signal<Post[]>([]);
      nextPage = signal<number | null>(null);
      loading = signal(true);

      constructor(private butter: ButterService) {}

      async ngOnInit() {
        await this.loadPosts(1);
      }

      async loadPosts(page: number | null) {
        if (page === null) {
          return;
        }
        this.loading.set(true);
        try {
          const response = await this.butter.getPosts({ page, page_size: 10 });
          if (!response.data?.data) {
            throw new Error('Failed to load posts from ButterCMS');
          }
          this.posts.set(response.data.data);
          this.nextPage.set(response.data.meta?.next_page ?? null);
        } finally {
          this.loading.set(false);
        }
      }
    }
    ```

    ```html theme={null}
    <!-- src/app/pages/blog/blog.page.html -->
    <ion-header>
      <ion-toolbar>
        <ion-title>Blog</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <div *ngIf="loading()" class="ion-text-center ion-padding">
        <ion-spinner></ion-spinner>
      </div>

      <ion-list *ngIf="!loading()">
        <ion-item
          *ngFor="let post of posts()"
          [routerLink]="['/blog', post.slug]"
          button
        >
          <ion-label>
            <h2>{{ post.title }}</h2>
            <p>By {{ post.author?.first_name }} {{ post.author?.last_name }}</p>
            <p [innerHTML]="post.summary"></p>
          </ion-label>
        </ion-item>
      </ion-list>

      <div *ngIf="nextPage()" class="ion-padding">
        <ion-button expand="block" (click)="loadPosts(nextPage())">Load More</ion-button>
      </div>
    </ion-content>
    ```
  </Tab>

  <Tab title="Single Blog Post">
    ```typescript theme={null}
    // src/app/pages/blog-post/blog-post.page.ts
    import { Component, OnInit, signal } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { ButterService } from '../../services/butter.service';

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

    @Component({
      selector: 'app-blog-post',
      standalone: false,
      templateUrl: './blog-post.page.html',
    })
    export class BlogPostPage implements OnInit {
      post = signal<Post | null>(null);
      loading = signal(true);
      error = signal<string | null>(null);

      constructor(
        private route: ActivatedRoute,
        private butter: ButterService
      ) {}

      async ngOnInit() {
        const slug = this.route.snapshot.paramMap.get('slug');
        if (!slug) {
          this.error.set('Post not found');
          this.loading.set(false);
          return;
        }
        try {
          const response = await this.butter.getPost(slug);
          if (!response.data?.data) {
            throw new Error('Failed to load post from ButterCMS');
          }
          this.post.set(response.data.data);
        } catch {
          this.error.set('Post not found');
        } finally {
          this.loading.set(false);
        }
      }
    }
    ```

    ```html theme={null}
    <!-- src/app/pages/blog-post/blog-post.page.html -->
    <ion-header>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-back-button defaultHref="/blog"></ion-back-button>
        </ion-buttons>
        <ion-title>{{ post()?.title }}</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <div *ngIf="loading()" class="ion-text-center ion-padding">
        <ion-spinner></ion-spinner>
      </div>

      <div *ngIf="error() as err" class="ion-padding">
        <p>{{ err }}</p>
      </div>

      <article *ngIf="post() as p" class="ion-padding">
        <ng-container *ngIf="!loading()">
          <h1>{{ p.title }}</h1>
          <p>
            By {{ p.author?.first_name }} {{ p.author?.last_name }}
          </p>
          <img
            *ngIf="p.featured_image"
            [src]="p.featured_image"
            [alt]="p.title"
            style="width: 100%"
          />
          <div [innerHTML]="p.body"></div>
        </ng-container>
      </article>
    </ion-content>
    ```
  </Tab>
</Tabs>

## Resources

<CardGroup cols={2}>
  <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">
    View source code
  </Card>

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