Skip to main content

Fetching Repeater data via API

This guide provides comprehensive code examples for fetching and rendering Repeater data from ButterCMS across various programming languages and frameworks.
Need to set up repeaters first? See Creating Repeater Fields.

API response structure

When you publish a page with a Repeater field, the API returns your content as an array of items. Here are examples of common Repeater field responses:
{
  "features": [
    {
      "title": "Fast Performance",
      "description": "Lightning quick load times",
      "icon": "https://cdn.buttercms.com/icon1.svg"
    },
    {
      "title": "Easy Integration",
      "description": "Works with any tech stack",
      "icon": "https://cdn.buttercms.com/icon2.svg"
    },
    {
      "title": "Great Support",
      "description": "24/7 expert assistance",
      "icon": "https://cdn.buttercms.com/icon3.svg"
    }
  ]
}
Each Repeater field returns as an array of items. As you add more items to a Repeater in the CMS, more objects appear in the array. A developer can map over that list and render the items in any way they want.

JavaScript / Node.js

Using the ButterCMS JavaScript SDK

const butter = require('buttercms')('your-api-token');

async function getPageWithRepeaters() {
  try {
    const response = await butter.page.retrieve('*', 'landing-page');
    const page = response.data.data;

    // Access repeater data
    const features = page.fields.features;
    const testimonials = page.fields.testimonials;

    // Iterate over features
    features.forEach(feature => {
      console.log(`Feature: ${feature.title}`);
      console.log(`Description: ${feature.description}`);
      console.log(`Icon: ${feature.icon}`);
    });

    return page;
  } catch (error) {
    console.error('Error fetching page:', error);
  }
}

Using Fetch API (Vanilla JavaScript)

const API_TOKEN = 'your-api-token';
const PAGE_SLUG = 'landing-page';

async function fetchRepeaterData() {
  const url = `https://api.buttercms.com/v2/pages/*/${PAGE_SLUG}/?auth_token=${API_TOKEN}`;

  const response = await fetch(url);
  const data = await response.json();

  const features = data.data.fields.features;

  // Render to DOM
  const container = document.getElementById('features-container');
  container.innerHTML = features.map(feature => `
    <div class="feature-card">
      <img src="${feature.icon}" alt="${feature.title}" />
      <h3>${feature.title}</h3>
      <p>${feature.description}</p>
    </div>
  `).join('');
}

React

React Component for Repeater data

import { useState, useEffect } from 'react';
import Butter from 'buttercms';

const butter = Butter('your-api-token');

function FeaturesSection() {
  const [features, setFeatures] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchFeatures() {
      try {
        const response = await butter.page.retrieve('*', 'landing-page');
        setFeatures(response.data.data.fields.features);
      } catch (error) {
        console.error('Error:', error);
      } finally {
        setLoading(false);
      }
    }
    fetchFeatures();
  }, []);

  if (loading) return <div>Loading...</div>;

  return (
    <section className="features">
      <h2>Our Features</h2>
      <div className="features-grid">
        {features.map((feature, index) => (
          <div key={index} className="feature-card">
            <img src={feature.icon} alt="" className="feature-icon" />
            <h3>{feature.title}</h3>
            <p>{feature.description}</p>
          </div>
        ))}
      </div>
    </section>
  );
}

export default FeaturesSection;
import { useState } from 'react';

function ImageCarousel({ images }) {
  const [currentIndex, setCurrentIndex] = useState(0);

  const goToNext = () => {
    setCurrentIndex((prev) => (prev + 1) % images.length);
  };

  const goToPrevious = () => {
    setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
  };

  return (
    <div className="carousel">
      <button onClick={goToPrevious} aria-label="Previous"></button>

      <div className="carousel-slide">
        <img
          src={images[currentIndex].image}
          alt={images[currentIndex].alt_text || images[currentIndex].title}
        />
        <p className="caption">{images[currentIndex].caption}</p>
      </div>

      <button onClick={goToNext} aria-label="Next"></button>

      <div className="carousel-dots">
        {images.map((_, index) => (
          <button
            key={index}
            className={index === currentIndex ? 'active' : ''}
            onClick={() => setCurrentIndex(index)}
            aria-label={`Go to slide ${index + 1}`}
          />
        ))}
      </div>
    </div>
  );
}

Testimonials Component

function Testimonials({ testimonials }) {
  return (
    <section className="testimonials">
      <h2>What Our Customers Say</h2>
      <div className="testimonials-grid">
        {testimonials.map((testimonial, index) => (
          <div key={index} className="testimonial-card">
            <blockquote>"{testimonial.quote}"</blockquote>
            <div className="author">
              {testimonial.author_photo && (
                <img
                  src={testimonial.author_photo}
                  alt={testimonial.author_name}
                  className="author-photo"
                />
              )}
              <div className="author-info">
                <strong>{testimonial.author_name}</strong>
                <span>
                  {testimonial.author_title}, {testimonial.author_company}
                </span>
              </div>
            </div>
            {testimonial.rating && (
              <div className="rating">
                {'⭐'.repeat(testimonial.rating)}
              </div>
            )}
          </div>
        ))}
      </div>
    </section>
  );
}

Vue.js

Vue 3 composition API

<template>
  <section v-if="!loading" class="features">
    <h2>Our Features</h2>
    <div class="features-grid">
      <div
        v-for="(feature, index) in features"
        :key="index"
        class="feature-card"
      >
        <img :src="feature.icon" :alt="feature.title" class="feature-icon" />
        <h3>{{ feature.title }}</h3>
        <p>{{ feature.description }}</p>
      </div>
    </div>
  </section>
  <div v-else>Loading...</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import Butter from 'buttercms';

const butter = Butter('your-api-token');
const features = ref([]);
const loading = ref(true);

onMounted(async () => {
  try {
    const response = await butter.page.retrieve('*', 'landing-page');
    features.value = response.data.data.fields.features;
  } catch (error) {
    console.error('Error:', error);
  } finally {
    loading.value = false;
  }
});
</script>

FAQ accordion Component

<template>
  <section class="faq">
    <h2>Frequently Asked Questions</h2>
    <div class="faq-list">
      <div
        v-for="(item, index) in faqItems"
        :key="index"
        class="faq-item"
      >
        <button
          class="faq-question"
          @click="toggleItem(index)"
          :aria-expanded="openItems.includes(index)"
        >
          {{ item.question }}
          <span class="icon">{{ openItems.includes(index) ? '−' : '+' }}</span>
        </button>
        <div v-show="openItems.includes(index)" class="faq-answer">
          <p>{{ item.answer }}</p>
        </div>
      </div>
    </div>
  </section>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  faqItems: {
    type: Array,
    required: true
  }
});

const openItems = ref([]);

const toggleItem = (index) => {
  const itemIndex = openItems.value.indexOf(index);
  if (itemIndex > -1) {
    openItems.value.splice(itemIndex, 1);
  } else {
    openItems.value.push(index);
  }
};
</script>

Testimonials Component

<template>
  <section class="testimonials">
    <div
      v-for="(testimonial, index) in testimonials"
      :key="index"
      class="testimonial-card"
    >
      <blockquote>{{ testimonial.quote }}</blockquote>
      <div class="author">
        <img :src="testimonial.author_photo" :alt="testimonial.author_name" />
        <div>
          <strong>{{ testimonial.author_name }}</strong>
          <span>{{ testimonial.author_title }}, {{ testimonial.author_company }}</span>
        </div>
      </div>
      <div class="rating">
        <span v-for="n in testimonial.rating" :key="n"></span>
      </div>
    </div>
  </section>
</template>

<script setup>
const props = defineProps({
  testimonials: Array
});
</script>

Python

Using the ButterCMS Python SDK

from butter_cms import ButterCMS

client = ButterCMS('your-api-token')

def get_page_with_repeaters():
    response = client.pages.get('*', 'landing-page')
    page = response['data']

    # Access repeater data
    features = page['fields']['features']

    for feature in features:
        print(f"Feature: {feature['title']}")
        print(f"Description: {feature['description']}")
        print(f"Icon: {feature['icon']}")
        print("---")

    return page

Flask example

from flask import Flask, render_template
from butter_cms import ButterCMS

app = Flask(__name__)
client = ButterCMS('your-api-token')

@app.route('/features')
def features_page():
    response = client.pages.get('*', 'features-page')
    features = response['data']['fields']['features']
    return render_template('features.html', features=features)
<!-- templates/features.html -->
<section class="features">
  <h2>Our Features</h2>
  <div class="features-grid">
    {% for feature in features %}
    <div class="feature-card">
      <img src="{{ feature.icon }}" alt="{{ feature.title }}">
      <h3>{{ feature.title }}</h3>
      <p>{{ feature.description }}</p>
    </div>
    {% endfor %}
  </div>
</section>

Django example

# views.py
from django.shortcuts import render
from butter_cms import ButterCMS

client = ButterCMS('your-api-token')

def landing_page(request):
    response = client.pages.get('*', 'landing-page')
    page = response['data']

    context = {
        'page_title': page['fields']['page_title'],
        'features': page['fields']['features'],
        'testimonials': page['fields']['testimonials'],
    }
    return render(request, 'landing_page.html', context)
<!-- templates/landing_page.html -->
<h1>{{ page_title }}</h1>

<section class="features">
  {% for feature in features %}
  <div class="feature-card">
    <img src="{{ feature.icon }}" alt="{{ feature.title }}">
    <h3>{{ feature.title }}</h3>
    <p>{{ feature.description }}</p>
  </div>
  {% endfor %}
</section>

<section class="testimonials">
  {% for testimonial in testimonials %}
  <blockquote>
    <p>"{{ testimonial.quote }}"</p>
    <cite>— {{ testimonial.author_name }}, {{ testimonial.author_company }}</cite>
  </blockquote>
  {% endfor %}
</section>

Jinja2 template

# views.py
from butter_cms import ButterCMS

client = ButterCMS('your-api-token')
response = client.pages.get('*', 'landing-page')
page = response['data']['fields']
<!-- Jinja2 template -->
<section class="faq">
  {% for item in page.faq_items %}
  <div class="faq-item">
    <h3 class="question">{{ item.question }}</h3>
    <div class="answer">{{ item.answer }}</div>
  </div>
  {% endfor %}
</section>

Go

Processing features Repeater in Go

package main

import (
    "fmt"
    ButterCMS "github.com/ButterCMS/buttercms-go"
)

type Feature struct {
    Headline    string `json:"headline"`
    Description string `json:"description"`
}

func ProcessFeaturesSection(section map[string]interface{}) []Feature {
    unparsedFeatures, _ := section["features"].([]interface{})
    features := []Feature{}

    for _, unparsedFeature := range unparsedFeatures {
        typedFeature := unparsedFeature.(map[string]interface{})
        headline, _ := typedFeature["headline"].(string)
        description, _ := typedFeature["description"].(string)

        feature := Feature{
            Headline:    headline,
            Description: description,
        }
        features = append(features, feature)
    }
    return features
}

Complete Go handler

package main

import (
    "html/template"
    "net/http"
    "os"

    ButterCMS "github.com/ButterCMS/buttercms-go"
)

type LandingPage struct {
    PageTitle string
    Features  []Feature
}

func landingPageHandler(w http.ResponseWriter, r *http.Request) {
    ButterCMS.SetAuthToken(os.Getenv("BUTTERCMS_API_TOKEN"))

    result, err := ButterCMS.GetPage("*", "landing-page", nil)
    if err != nil {
        http.Error(w, "Error fetching page", http.StatusInternalServerError)
        return
    }

    pageData := LandingPage{
        PageTitle: result.Page.Fields["page_title"].(string),
        Features:  ProcessFeaturesSection(result.Page.Fields),
    }

    tmpl := template.Must(template.ParseFiles("templates/landing.html"))
    tmpl.Execute(w, pageData)
}

PHP / Laravel

Using the ButterCMS PHP SDK

<?php
require_once 'vendor/autoload.php';

use ButterCMS\ButterCMS;

$butter = new ButterCMS('your-api-token');

$response = $butter->fetchPage('*', 'landing-page');
$page = $response->getData();

$features = $page['fields']['features'];

foreach ($features as $feature) {
    echo "<div class='feature-card'>";
    echo "<img src='{$feature['icon']}' alt='{$feature['title']}'>";
    echo "<h3>{$feature['title']}</h3>";
    echo "<p>{$feature['description']}</p>";
    echo "</div>";
}

Laravel controller

<?php

namespace App\Http\Controllers;

use ButterCMS\ButterCMS;

class PageController extends Controller
{
    protected $butter;

    public function __construct()
    {
        $this->butter = new ButterCMS(env('BUTTERCMS_API_TOKEN'));
    }

    public function landing()
    {
        $response = $this->butter->fetchPage('*', 'landing-page');
        $page = $response->getData();

        return view('landing', [
            'pageTitle' => $page['fields']['page_title'],
            'features' => $page['fields']['features'],
            'testimonials' => $page['fields']['testimonials'],
        ]);
    }
}

Laravel Blade template

<!-- resources/views/landing.blade.php -->
<h1>{{ $pageTitle }}</h1>

<section class="features">
    @foreach ($features as $feature)
    <div class="feature-card">
        <img src="{{ $feature['icon'] }}" alt="{{ $feature['title'] }}">
        <h3>{{ $feature['title'] }}</h3>
        <p>{{ $feature['description'] }}</p>
    </div>
    @endforeach
</section>

<section class="testimonials">
    @foreach ($testimonials as $testimonial)
    <blockquote>
        <p>"{{ $testimonial['quote'] }}"</p>
        <cite>{{ $testimonial['author_name'] }}, {{ $testimonial['author_company'] }}</cite>
    </blockquote>
    @endforeach
</section>

Ruby / Rails

Using the ButterCMS Ruby SDK

require 'buttercms-ruby'

ButterCMS::api_token = 'your-api-token'

page = ButterCMS::Page.find('*', 'landing-page')
features = page.fields[:features]

features.each do |feature|
  puts "Feature: #{feature[:title]}"
  puts "Description: #{feature[:description]}"
  puts "---"
end

Rails controller

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def landing
    page = ButterCMS::Page.find('*', 'landing-page')
    @page_title = page.fields[:page_title]
    @features = page.fields[:features]
    @testimonials = page.fields[:testimonials]
  end
end

Rails view (ERB)

<!-- app/views/pages/landing.html.erb -->
<h1><%= @page_title %></h1>

<section class="features">
  <% @features.each do |feature| %>
  <div class="feature-card">
    <img src="<%= feature[:icon] %>" alt="<%= feature[:title] %>">
    <h3><%= feature[:title] %></h3>
    <p><%= feature[:description] %></p>
  </div>
  <% end %>
</section>

<section class="testimonials">
  <% @testimonials.each do |testimonial| %>
  <blockquote>
    <p>"<%= testimonial[:quote] %>"</p>
    <cite><%= testimonial[:author_name] %>, <%= testimonial[:author_company] %></cite>
  </blockquote>
  <% end %>
</section>

Repeater structure

Repeaters are single-level lists in this codebase. Repeater subfields can only be standard field types.

Error handling best practices

async function fetchPageSafely(pageSlug) {
  try {
    const response = await butter.page.retrieve('*', pageSlug);

    // Check if repeater exists and has items
    const features = response.data?.data?.fields?.features;

    if (!features || !Array.isArray(features)) {
      console.warn('Features repeater is empty or missing');
      return [];
    }

    return features;
  } catch (error) {
    if (error.response?.status === 404) {
      console.error('Page not found');
    } else if (error.response?.status === 401) {
      console.error('Invalid API token');
    } else {
      console.error('Unexpected error:', error);
    }
    return [];
  }
}