Choosing a Content API Shape for SEO Builds

The shape of your content queries decides whether a page can be rendered completely on the server in one round trip — and that single property determines most of what follows in a decoupled build.

When to Use This Approach

Evaluate the API shape when:

  • Build times are growing faster than content volume, which points at a request waterfall.
  • Templates fetch content client-side because “the API call is too slow for the server render”.
  • A new content type is being added and its query pattern is about to be established by default.

The Waterfall Is the Problem

A template that fetches an entry, then its author, then its category, then each image serialises four round trips per page. At thirty thousand pages that is the whole build.

Request waterfall compared with a single composed query Four sequential API calls each waiting on the previous, totalling four round trips per page, against one composed query returning the entry and its references in a single round trip. waterfall entry author category assets 4 round trips per page — build time scales with latency composed entry + references + assets 1 round trip per page — build time scales with content volume

Implementation Steps

Step 1: Enumerate what one render actually needs

# templates/article.requirements.yml
fields:
  - title
  - standfirst
  - body            # rich text with embedded references
  - publishedAt
  - updatedAt
references:
  author:   [name, slug, path, avatar]
  category: [title, slug, path]
  hero:     [url, alt, width, height]
  related:  [title, path]        # 3 items, shallow
metadata:
  - seo.title
  - seo.description
  - seo.noindex
  - path
  - previousPaths

Validation: Render the template with only these fields available. Anything that breaks is a requirement you missed; anything unused is payload you are paying for on every page.

Step 2: Measure the current round trips

// Instrument the CMS client during a single page render
let calls = 0;
const timings = [];
export async function instrumentedFetch(query, vars) {
  const t0 = performance.now();
  const res = await cmsFetch(query, vars);
  timings.push({ n: ++calls, ms: performance.now() - t0, query: query.slice(0, 40) });
  return res;
}

// After rendering one page
console.table(timings);
console.log(`total ${calls} calls, ${timings.reduce((a, t) => a + t.ms, 0).toFixed(0)} ms`);

Validation: Multiply the total by your route count. If the result exceeds your acceptable build time, the API shape is the constraint — not the renderer, not the hosting.

Step 3: Collapse to one query per page

query ArticlePage($path: String!) {
  article(where: { path: $path }) {
    title
    standfirst
    body { json links { assets { url alt width height } entries { title path } } }
    publishedAt
    updatedAt
    path
    seo { title description noindex }
    author   { name slug path avatar { url width height } }
    category { title slug path }
    hero     { url alt width height }
    related(limit: 3) { title path }
  }
}

SEO impact: The whole page — content, metadata, structured-data inputs, and internal links — becomes available before rendering starts, which is the precondition for complete server HTML.

Validation: Re-run Step 2. The call count should be exactly one.

Step 4: Batch the build

Per-page queries at build time still pay one round trip per page. Fetching in batches converts that to one per hundred.

// scripts/fetch-all.mjs
const PAGE_SIZE = 100;

export async function* allArticles() {
  let cursor = null;
  do {
    const { items, nextCursor } = await cms.query(ARTICLE_PAGE_BATCH, {
      first: PAGE_SIZE,
      after: cursor,
    });
    yield* items;
    cursor = nextCursor;
  } while (cursor);
}

SEO impact: Build duration becomes a function of content volume and payload size rather than of network latency, which is what makes a large catalogue buildable at all.

Validation: Time a build at 1,000 and 10,000 entries. The relationship should be close to linear with a small constant, not dominated by a per-page constant.

Step 5: Cache by content hash

// lib/build-cache.js
import { createHash } from 'node:crypto';

export function cacheKey(entry) {
  const payload = JSON.stringify({
    id: entry.id,
    updatedAt: entry.updatedAt,
    refs: collectReferences(entry).map((r) => [r.id, r.updatedAt]),
  });
  return createHash('sha1').update(payload).digest('hex').slice(0, 16);
}

export async function renderCached(entry, render) {
  const key = cacheKey(entry);
  const hit = await cache.get(key);
  if (hit) return hit;
  const html = await render(entry);
  await cache.set(key, html);
  return html;
}

SEO impact: Including reference timestamps in the key means a shared asset change correctly invalidates every page that renders it — the same rollup logic that makes lastmod accurate.

Validation: Run two consecutive builds with no content change. The second should be almost entirely cache hits.

Choosing Between API Styles

The question is not which technology but which of three properties your build needs most.

Comparing content API styles on the properties that matter for builds REST without includes, REST with includes, and GraphQL compared across round trips per page, payload control, and cacheability. style round trips payload control edge caching REST, no includes one per reference whole records simple, by URL REST with includes one per page coarse simple, by URL GraphQL one per page exact fields needs persisted queries Round trips per page is the property that decides build economics A REST API with includes is often sufficient — the win is composition, not the query language

SEO Impact Summary

Build duration is the ceiling on content freshness, so the API shape ultimately decides how current your indexed content can be.

Build duration as the freshness ceiling A chain from API round trips to build duration to publish-to-index latency, showing each term bounding the next. round trips per page build duration publish to index cannot be shorter No amount of content investment can beat the ceiling the API shape sets
Signal What improves What breaks if misconfigured
Server rendering Complete HTML is producible because all data arrives before render A slow API pushes teams toward client fetching, emptying the server response
Content freshness Short builds mean updates reach the index within hours Waterfall builds bound freshness to the deploy cadence
Crawl efficiency Fast origin responses sustain a higher crawl rate Per-request API latency shows up directly in time to first byte
Link discovery Referenced entries arrive with the entry, so links render server-side Missing reference data turns internal links into client-rendered placeholders

Measurable signals to watch:

  • API calls per page render, asserted at one in a test.
  • Build wall-clock time per thousand entries, trended across releases.
  • Cache-hit ratio on consecutive builds with no content change.

Edge Cases and Gotchas

Query complexity limits will stop a naive composed query Most hosted CMS APIs cap query depth or complexity. Split by concern rather than by reference — one query for the page, one for a large related collection — instead of returning to a per-reference waterfall.

Rich text with embedded entries needs its links resolved in the same call If embedded references come back as identifiers you then resolve individually, you have reintroduced the waterfall inside the body field. Request the resolved links alongside the rich-text document.

Preview queries are a different shape Draft content usually bypasses the CDN and the published index, so preview queries are slower and less cacheable. Keep them on a separate code path so preview latency never influences the production query shape — the isolation reasoning is in preview & draft content SEO.

Field selection is also a payload-size decision at the edge Requesting the whole record because it is convenient inflates every cached response. On a large catalogue the difference between a selected and an unselected query can be gigabytes of cache occupancy.

Frequently Asked Questions

Is GraphQL better than REST for a headless SEO build? For rendering complete pages, usually — one query returns an entity and its references at the depth the template needs, removing the waterfall. The advantage is the single round trip rather than the language; a REST API with an include or depth parameter achieves the same.

How deep should a content query go? Exactly as deep as the template renders. Two or three levels covers most pages: entry, referenced entries, their assets. Deeper inflates payloads and build times; shallower produces a second round trip that costs more than the fields would have.

Does the API shape actually affect rankings? Not directly, but it sets a ceiling on two things that do. A slow API pushes content out of the server HTML, and long builds bound content freshness to your deploy cadence. Both show up in indexing outcomes.

What about pages that genuinely need live data? Split the page: render the stable content statically and fetch the volatile fields separately, either streamed or client-side. Price and stock are the canonical example — neither is required for the page to be indexable, and neither is ever the largest contentful element.


Part of: Composable CMS Architecture Basics

Related