Headless Apps
Craft Cloud uses advanced bot detection and makes a best effort to prioritize human traffic. This poses a challenge for headless apps: all content retrieval is automated and often arrives in concentrated bursts during static builds and background revalidation.
Follow these guidelines for a successful headless setup on Craft Cloud:
- Request signing: Sign requests within your hosting platform, such as Vercel or Netlify, to bypass the stricter untrusted-bot policy. Never expose the signing key to browser code or a public environment variable.
- Automated retries: Retries provide resilience against unavoidable
transient network errors, not just rate limits. Rate limits exist to protect
your origin. Without them, traffic bursts could overwhelm your database and
result in more problematic errors.
- Automated builds can issue many requests in a short window. If possible, slow the request rate by reducing build concurrency or adding an interval between requests. Nuxt’s Nitro engine (opens new window) (no relation (opens new window)) supports both options.
- When possible, send GraphQL queries with
GETrequests so successful responses can be cached by your hosting platform. - For retryable error responses, honor
Retry-After, ideally with exponential backoff. - Only retry
POSTrequests that contain read-only GraphQL queries—never mutations.
#Automated Retries
Resilient automated requests should handle network errors, Retry-After,
exponential backoff, and jitter. For brevity, the examples below use
Ky (opens new window) for this policy, but no dependency
is required.
#Request Signatures
Create a request-signatures.js module using getSignatureHeaders() from the
general Node.js signing example. The
framework examples below construct and sign a native Request before sending
it.
#Next.js Example
Next.js can cache (opens new window) the validated result of a data-fetching function. This example uses Ky (opens new window) for the underlying request:
import ky from 'ky';
import { unstable_cache } from 'next/cache';
import { getSignatureHeaders } from './request-signatures.js';
const { CRAFT_URL, CRAFT_GRAPHQL_TOKEN } = process.env;
const method = 'POST';
const url = `${CRAFT_URL}/api`;
const query = `{ entries(section: "blog") { title url } }`;
const body = JSON.stringify({ query });
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRAFT_GRAPHQL_TOKEN}`,
};
const getBlogEntries = unstable_cache(
async () => {
const request = new Request(url, { method, body, headers });
for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}
const result = await ky(request, {
cache: 'no-store',
retry: {
limit: 10,
methods: ['get', 'post'],
jitter: true,
},
timeout: false,
totalTimeout: 30_000,
}).json();
if (result.errors?.length) {
throw new Error(result.errors.map((error) => error.message).join('\n'));
}
return result.data;
},
['craft:blog', url, query],
{
revalidate: 300,
tags: ['craft:blog'],
}
);
const data = await getBlogEntries();
Use narrow tags such as craft:blog or craft:products, and avoid bursts of
app-wide invalidations. When revalidation throws, Next.js continues serving
the last successful result and tries again on a later request.
Vercel provides stale-while-revalidate behavior through
ISR (opens new window). Netlify’s
current Next.js adapter (opens new window)
also supports the Full Route and Data caches, including tag- and path-based
revalidation.
#Nuxt Example
Keep the signed request in a Nuxt server route:
// server/api/blog.get.js
import ky from 'ky';
import { getSignatureHeaders } from '../utils/request-signatures.js';
const { CRAFT_URL, CRAFT_GRAPHQL_TOKEN } = process.env;
const method = 'POST';
const url = `${CRAFT_URL}/api`;
const query = `{ entries(section: "blog") { title url } }`;
const body = JSON.stringify({ query });
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRAFT_GRAPHQL_TOKEN}`,
};
export default defineEventHandler(async () => {
const request = new Request(url, { method, body, headers });
for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}
const result = await ky(request, {
retry: {
limit: 10,
methods: ['get', 'post'],
jitter: true,
},
timeout: false,
totalTimeout: 30_000,
}).json();
if (result.errors?.length) {
throw new Error(result.errors.map((error) => error.message).join('\n'));
}
return result.data;
});
Apply stale-while-revalidate caching with a route rule, then call the route
from your components with useFetch('/api/blog'):
// nuxt.config.js
export default defineNuxtConfig({
routeRules: {
'/api/blog': { swr: 300 },
},
});
Nuxt also supports an isr route rule on Vercel and Netlify, but adapter
behavior differs. Netlify currently documents a
cache-control limitation for Nuxt ISR routes (opens new window),
so verify the deployed response before relying on CDN caching.
#Astro Example
Astro prerenders pages by default, so a failed Craft request should fail the build rather than publish partial content:
---
import ky from 'ky';
import { getSignatureHeaders } from '../lib/request-signatures.js';
const { CRAFT_URL, CRAFT_GRAPHQL_TOKEN } = process.env;
const method = 'POST';
const url = `${CRAFT_URL}/api`;
const query = `{ entries(section: "blog") { title url } }`;
const body = JSON.stringify({ query });
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CRAFT_GRAPHQL_TOKEN}`,
};
const request = new Request(url, { method, body, headers });
for (const [name, value] of Object.entries(getSignatureHeaders(request))) {
request.headers.set(name, value);
}
const result = await ky(request, {
retry: {
limit: 10,
methods: ['get', 'post'],
jitter: true,
},
timeout: false,
totalTimeout: 30_000,
}).json();
if (result.errors?.length) {
throw new Error(result.errors.map((error) => error.message).join('\n'));
}
const data = result.data;
---
Netlify uses atomic deploys (opens new window), and Vercel promotes successful deployments to production. A failed build therefore leaves the current production app in place.
For on-demand rendering, Astro 7 provides a
route cache API (opens new window) with
stale-while-revalidate semantics.