Paginating Ghost's Content API Without Losing Your Mind (or Your Rate Limit)
Ghost's Content API paginates, filters, and sorts server‑side — if you drive your fetch loop off meta.pagination instead of guessing. A worked archive‑page example, plus the rate‑limit caveats.
18 Mar 2026, 10:00 UTC

What the API actually gives you
Ghost's Content API (the read‑only, key‑authenticated flavor) exposes /ghost/api/content/posts/ with query parameters that do the heavy lifting on the server:
filter— narrow results by fields likestatus,tag,author, orfeaturedusing Ghost's filter syntax (e.g.,filter=tag:news+featured:true).order— sort by a field and direction, such asorder=published_at%20desc.pageandlimit— classic offset pagination. The default limit is 15 per page in current versions (older docs reference 10), so check your version rather than assuming.
Every collection response includes a meta.pagination object with page, limit, pages, total, next, and prev. That object is your source of truth — next is null when you're done, so you never have to guess whether another page exists.
A worked example: building an archive page
Say you want every published post tagged engineering, newest first, for a statically generated archive. Run this wherever your build script executes — it only needs the public content API key, no admin credentials:
const BASE = 'https://yourblog.example.com/ghost/api/content';
const KEY = process.env.GHOST_CONTENT_KEY;
async function fetchAllEngineeringPosts() {
const posts = [];
let page = 1;
while (true) {
const url = new URL(`${BASE}/posts/`);
url.search = new URLSearchParams({
key: KEY,
filter: 'tag:engineering',
order: 'published_at desc',
limit: '50',
page: String(page),
fields: 'title,slug,published_at,excerpt',
});
const res = await fetch(url);
if (!res.ok) throw new Error(`Ghost API ${res.status} on page ${page}`);
const data = await res.json();
posts.push(...data.posts);
if (data.meta.pagination.next === null) break;
page = data.meta.pagination.next;
}
return posts;
}Three decisions here are worth noting. First, the loop terminates on pagination.next === null, not on an empty array — that's the documented contract. Second, fields trims the payload to what the archive actually renders, which matters when posts carry full HTML bodies. Third, the filter runs server‑side, so you're not downloading 500 posts to keep 40.
To verify it works, hit /ghost/api/content/posts/?key=YOUR_KEY&page=1&limit=5 in a browser and confirm the JSON contains meta.pagination with sensible total and pages values. If total doesn't match your published post count, check whether drafts or pages are leaking into your expectation.
The trade‑offs you'll actually hit
Rate limits. Content API requests are rate‑limited per IP, and a pagination loop is exactly the kind of burst that trips limits on busy sites. For a build‑time archive this is usually fine — a handful of sequential requests — but if you're paginating client‑side on every page view, cache aggressively or move fetching to build time. Add a small delay or backoff on 429 responses if you loop over many pages.
Big limits aren't a shortcut. You can raise limit to reduce round trips, but very large pages produce slow, heavy responses and can still be capped by server configuration. Fifty is a reasonable middle ground; measure before going higher.
Offset pagination drifts. If posts are published while you paginate, items can shift between pages. For build‑time snapshots this rarely matters; for live infinite‑scroll feeds, expect occasional duplicates or gaps and dedupe by id.
Version drift. This behavior reflects the stable Content API design (v3 through current v5 share this pagination model), but parameter defaults and filter syntax details change between major versions. Pin the version in your URL if your Ghost install is older, and confirm against the official Content API reference for your Ghost version before shipping.
What to do next
Start with one filtered query in the browser and inspect meta.pagination. Then write the loop keyed off pagination.next, add fields to slim the payload, and wrap it in a retry for 429s. You'll have an archive that scales from ten posts to ten thousand without changing shape — and you'll never again hardcode limit=10000 and hope.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.