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:- Basic repeater
- Image gallery
- Testimonials
- Image carousel
{
"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"
}
]
}
{
"gallery": [
{
"image": "https://cdn.buttercms.com/photo1.jpg",
"caption": "Team meeting in progress",
"alt_text": "Five team members collaborating around a table"
},
{
"image": "https://cdn.buttercms.com/photo2.jpg",
"caption": "Our modern office space",
"alt_text": "Open office with natural lighting"
}
]
}
{
"testimonials": [
{
"quote": "ButterCMS transformed our content workflow!",
"author_name": "Jane Smith",
"author_title": "Marketing Director",
"author_company": "TechCorp",
"author_photo": "https://cdn.buttercms.com/jane.jpg",
"rating": 5
},
{
"quote": "The best CMS we've ever used.",
"author_name": "John Doe",
"author_title": "CTO",
"author_company": "StartupInc",
"author_photo": "https://cdn.buttercms.com/john.jpg",
"rating": 5
}
]
}
{
"slug": "example-page",
"page_type": null,
"status": "published",
"fields": {
"headline": "Carousel Example",
"image_carousel": [
{
"title": "Chicago",
"caption": "Thank you, Chicago!",
"image": "https://cdn.buttercms.com/WWANGqFQFeF1nWYOHaq7"
},
{
"title": "New York",
"caption": "Thanks New York!",
"image": "https://cdn.buttercms.com/KrO9ueULT6OoEVX9lqxw"
}
]
}
}
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;
Image carousel Component
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 [];
}
}