Skip to main content

Pagination methods

ButterCMS offers two pagination approaches, each suited to different use cases. These methods are mutually exclusive - use one or the other, not both.

Page-based pagination

Page-based pagination is the most common approach and is ideal for traditional “page 1, page 2, page 3” navigation patterns. Parameters:
  • page - Page number (default: 1, minimum: 1)
  • page_size - Number of items per page (default: 10, minimum: 1, maximum: 100)
Example Request:
# Fetch page 2 with 15 items per page
curl "https://api.buttercms.com/v2/posts/?page=2&page_size=15&auth_token=YOUR_TOKEN"
Response Structure:
{
  "meta": {
    "count": 45,
    "next_page": 3,
    "previous_page": 1
  },
  "data": [
    // Array of 15 blog posts
  ]
}
React Implementation:
import { useState, useEffect } from 'react';

function BlogList() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [meta, setMeta] = useState({});
  const pageSize = 10;

  useEffect(() => {
    async function fetchPosts() {
      const response = await fetch(
        `https://api.buttercms.com/v2/posts/?page=${page}&page_size=${pageSize}&exclude_body=true&auth_token=${API_TOKEN}`
      );
      const data = await response.json();
      setPosts(data.data);
      setMeta(data.meta);
    }
    fetchPosts();
  }, [page]);

  return (
    <div>
      {posts.map(post => (
        <article key={post.slug}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
        </article>
      ))}

      <nav>
        <button
          disabled={!meta.previous_page}
          onClick={() => setPage(meta.previous_page)}
        >
          Previous
        </button>
        <span>Page {page} of {Math.ceil(meta.count / pageSize)}</span>
        <button
          disabled={!meta.next_page}
          onClick={() => setPage(meta.next_page)}
        >
          Next
        </button>
      </nav>
    </div>
  );
}

Offset-based pagination

Offset-based pagination is ideal for infinite scroll implementations and more flexible navigation patterns. Parameters:
  • limit - Maximum number of items to return (default: 10, minimum: 1, maximum: 100)
  • offset - Number of items to skip before returning results (default: 0, minimum: 0)
Example Request:
# Skip first 20 items, fetch next 10
curl "https://api.buttercms.com/v2/posts/?limit=10&offset=20&auth_token=YOUR_TOKEN"
Response Structure:
{
  "meta": {
    "count": 45,
    "next_offset": 30,
    "previous_offset": 10
  },
  "data": [
    // Array of 10 blog posts
  ]
}
Infinite Scroll Implementation:
import { useState, useEffect, useRef, useCallback } from 'react';

function InfinitePostsList() {
  const [posts, setPosts] = useState([]);
  const [offset, setOffset] = useState(0);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  const limit = 10;
  const observer = useRef();

  const lastPostRef = useCallback(node => {
    if (loading) return;
    if (observer.current) observer.current.disconnect();

    observer.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting && hasMore) {
        setOffset(prev => prev + limit);
      }
    });

    if (node) observer.current.observe(node);
  }, [loading, hasMore]);

  useEffect(() => {
    async function fetchMore() {
      setLoading(true);
      const response = await fetch(
        `https://api.buttercms.com/v2/posts/?limit=${limit}&offset=${offset}&exclude_body=true&auth_token=${API_TOKEN}`
      );
      const data = await response.json();

      setPosts(prev => [...prev, ...data.data]);
      setHasMore(data.meta.next_offset !== null);
      setLoading(false);
    }
    fetchMore();
  }, [offset]);

  return (
    <div>
      {posts.map((post, index) => (
        <article
          key={post.slug}
          ref={index === posts.length - 1 ? lastPostRef : null}
        >
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
        </article>
      ))}
      {loading && <p>Loading more posts...</p>}
    </div>
  );
}

Choosing the right pagination method

Use CaseRecommended MethodReason
Traditional page navigationPage-basedIntuitive “Page X of Y” display
Infinite scrollOffset-basedEasy to append new items
Load more buttonOffset-basedSimple offset increment
SEO-friendly archivesPage-basedPredictable, crawlable URLs
Random access to pagesPage-basedDirect page number access

Filtering paginated results

To filter by author, category, tags, or custom field values, see Filtering Requests.

Combining pagination and filtering

// JavaScript SDK example
const butter = Butter('YOUR_API_TOKEN');

// Fetch page 1 of featured tutorials by a specific author
const response = await butter.post.list({
  page: 1,
  page_size: 10,
  author_slug: 'john-doe',
  category_slug: 'tutorials',
  exclude_body: true
});

console.log(`Found ${response.data.meta.count} posts`);
console.log(`Showing page 1 of ${Math.ceil(response.data.meta.count / 10)}`);

Performance tips for pagination

  • 10-25 items is optimal for most use cases
  • Larger page sizes increase response time and payload
  • Smaller page sizes (under 10) may require excessive requests for large content sets
  • Consider your content type: text-heavy items warrant smaller pages
For blog post listings, always include exclude_body=true:
# Good - 50-70% smaller response
/v2/posts/?page=1&page_size=10&exclude_body=true

# Avoid for listings - unnecessarily large response
/v2/posts/?page=1&page_size=10
See Query Optimization for more on exclude_body and levels.
Implement client-side or server-side caching for paginated results:
// Cache key includes all filter and pagination parameters
const cacheKey = `posts-page${page}-size${pageSize}-cat${category}`;

Building archive pages

A common use case is building blog archive pages with category and date filtering:
// Next.js archive page example
export async function getServerSideProps({ query }) {
  const butter = Butter(process.env.BUTTER_API_TOKEN);

  const page = parseInt(query.page) || 1;
  const pageSize = 12;
  const category = query.category || null;

  const params = {
    page,
    page_size: pageSize,
    exclude_body: true,
    ...(category && { category_slug: category })
  };

  const [posts, categories] = await Promise.all([
    butter.post.list(params),
    butter.category.list()
  ]);

  return {
    props: {
      posts: posts.data.data,
      meta: posts.data.meta,
      categories: categories.data.data,
      currentPage: page,
      currentCategory: category
    }
  };
}