Headless WordPress is often presented as a clean separation of responsibilities. WordPress manages the content, a modern frontend handles the user experience and the REST API passes structured data between the two. In principle, it is a sensible arrangement, especially for projects that need a familiar CMS behind a faster or more flexible frontend.
The difficulty is that the architecture does not end with the code. Between the deployed frontend and the CMS sits a less visible layer of DNS, hosting infrastructure, server-side rendering, edge runtimes, firewalls, bot protection and request reputation. That layer is easy to ignore when everything works, but it can quickly become the part of the system that decides whether the site has content or not.
A recent issue made this very clear. The WordPress CMS was working, the REST API endpoint was returning valid JSON when accessed directly and the frontend integration was structurally sound. From a browser, the API looked fine. The content was there and the endpoint responded as expected.
The live site, however, was still failing to display the content.
At first glance, that kind of failure looks like a frontend problem. It might be a mapping issue, a missing field, a stale environment variable or a broken API route. In this case, the problem was elsewhere. The deployed server-side runtime was requesting JSON from WordPress, but the hosting security layer was returning an HTML CAPTCHA challenge instead.
The CMS had not failed in the usual sense. WordPress was still capable of returning the correct data. The issue was that the live application was not being allowed to reach it cleanly.
The failure pattern
The most misleading part of this kind of issue is that manual testing can give you a false sense of certainty. A developer can open the WordPress REST API endpoint in a browser, see valid JSON and reasonably assume the CMS is not the problem. The endpoint responds, the content appears and the HTTP request looks successful enough to move attention back to the frontend.
The live application may tell a different story. Server-rendered pages show empty sections, project listings disappear or blog posts fail to load, even though the same CMS endpoint appears to work perfectly from a browser.
The first useful clue is often an error such as:
Unexpected token ‘<‘
That error is common enough to be easy to dismiss, but it is worth reading carefully. In many cases, it simply means the application expected JSON and received HTML. The opening < is usually the first character of an HTML document, which might be a CAPTCHA page, a bot protection challenge, a WAF interstitial, a redirect page, a hosting error page or some other server-generated response.
In a headless WordPress setup, this is awkward because the frontend is treating /wp-json/* as an API surface. It expects structured data. It does not expect the hosting layer to interrupt the request before WordPress responds.
From the frontend’s perspective, the API has failed. From WordPress’s perspective, the content is available. From the hosting provider’s perspective, the request may look suspicious enough to challenge. Each system is behaving according to its own rules, but the combined result is a broken live page.
That is why these problems can take longer to diagnose than a normal code issue. The fault sits between systems, rather than neatly inside one of them.
Why this is becoming more common
This problem is not specific to one host, one frontend platform or one WordPress installation. It is a side effect of how modern web projects are increasingly assembled.
More teams are using WordPress as a headless CMS because it remains a strong editorial platform. Clients know how to use it, developers can extend it and the REST API makes content available to other applications. For many projects, that is still a practical choice.
At the same time, more frontends are being deployed through modern platforms that use serverless functions, edge runtimes, server-side rendering and CDN-level infrastructure. These platforms are powerful, but they change the nature of the request being made to the CMS.
In a traditional WordPress site, the visitor’s browser requests a WordPress page directly. In a headless SSR setup, the deployed application may request the WordPress data from a server-side runtime before the page is sent to the visitor. That means the CMS no longer sees the visitor’s browser as the caller. It sees the hosting platform, the edge runtime or the serverless function.
That runtime may come from a shared IP range. It may use automated request headers or share network reputation with many unrelated projects. It may also sit behind infrastructure that changes depending on region, deployment or platform behaviour.
This is where server-level bot protection can become overzealous. Hosting providers are under pressure to block brute-force attacks, scraping, spam, automated abuse and malicious traffic. Many now use reputation-based or AI-assisted systems to decide whether a request should be allowed, challenged or blocked.
The problem is that legitimate server-side application traffic can look automated. A modern frontend fetching public CMS content can, from the origin server’s point of view, resemble the kind of non-human traffic these systems are designed to slow down.
When that happens, the CMS is not really unavailable. The request is being challenged before the CMS has the chance to return the data.
Why this affects tools like Lovable, Vercel, Netlify and Cloudflare
The same broad issue can appear across modern hosting and app-building platforms, especially where server-side rendering is involved.
Tools such as Lovable, Vercel, Netlify and Cloudflare make it much easier to build and deploy modern frontends. They are especially useful when you want a fast interface, server-side rendering, API routes, edge logic or AI-assisted development. They can also make a project feel simpler than it really is, because much of the infrastructure is abstracted away.
That abstraction is useful, but it does not remove the operational question underneath:
Can the deployed runtime reliably access the services it depends on?
That question matters more with SSR than with a simple client-rendered site. If the browser fetches content directly, the request comes from the visitor’s environment. If the page is rendered server-side, the request comes from the hosting platform. That difference can affect access, security rules, rate limits, bot protection and allowlisting.
It also explains why preview and production can behave differently. Local development may work because the request comes from your machine. A platform preview may work because it uses a slightly different runtime path. The published live site may fail because the production worker, function or edge process is being challenged by the CMS host.
This is also where AI-assisted development can create a false sense of completion. The generated code may be broadly correct, the endpoint may be right and the data shape may match the frontend. The site may even work in preview. But the deployed network path still has to be valid, and that is not something a prompt can guarantee.
This is not really an AI problem. It is a production architecture problem.
The hidden layer: origin security
In a headless setup, there are usually at least three systems involved: the CMS, the frontend host and the security layer between them. That security layer might be part of the CMS host, a web application firewall, Cloudflare, bot protection, a CDN, a reverse proxy or several of these at once.
The important point is that a public CMS endpoint is not automatically available to every kind of caller. A route such as /wp-json/wp/v2/posts may return JSON in a browser, while a server-side runtime receives a completely different response.
The host may decide that a particular IP address, request header, region, user agent or traffic pattern looks suspicious. In some cases, that challenge may not look like a hard failure. The application still receives a response, but it is an HTML security page rather than the JSON it expected.
If the frontend then calls response.json() without checking the response type, the real issue gets hidden behind a parsing error. The application appears to have a JSON problem, when the actual problem is that JSON was never returned.
That is why the first job is not to redesign the frontend or rewrite the CMS integration. The first job is to inspect the response the deployed application is actually receiving.
What developers should build in
A resilient headless WordPress setup needs a defensive integration layer. It is not enough to know that the REST endpoint works in a browser. The application should verify that the response it receives in production is actually the response it expected.
At a minimum, server-side CMS fetches should check the response status, the final URL after redirects, the content-type header, whether the response was redirected and whether the body appears to be JSON or HTML. When the response is not JSON, the logs should capture enough information to identify the problem without exposing sensitive data.
Useful diagnostics include the status code, content type, upstream URL, short response preview, server or CDN headers, request identifiers and the runtime environment making the request. This gives the frontend developer, hosting platform and CMS host something concrete to work with.
There is a large practical difference between saying “the feed is broken” and saying “the deployed runtime receives text/html from a public REST route that returns application/json in a browser.” The second statement narrows the issue immediately and points attention towards the origin security layer.
The application should also avoid silently swallowing errors and rendering empty content with no indication of what happened. Empty states are useful when there is genuinely no content. They are much less useful when the CMS fetch has failed in production.
Caching matters too
Short caching is often sensible for public CMS content. That does not mean caching everything aggressively or making editorial changes difficult to publish. It means giving the system enough breathing room that every page view does not trigger a fresh request from the SSR runtime to WordPress.
For marketing sites, portfolios, case studies and insight posts, a cache window of 60 to 300 seconds is often a reasonable trade-off. It reduces repeated origin requests, improves performance and lowers the chance that bursty traffic from a server-side runtime starts to look suspicious.
Caching can also provide a limited fallback. If the CMS fetch fails temporarily, the application may be able to serve the last successful response rather than rendering an empty page. That needs careful handling, particularly where content is time-sensitive, private or transactional, but for public editorial content it can be the difference between a graceful degradation and a broken page.
The key is to treat caching as part of the integration design, not as an afterthought added once the site is already failing.
When a proxy makes sense
A proxy should not be the default answer to every headless CMS problem, but it can be a useful control layer when the frontend and CMS sit on different infrastructure.
A narrow content proxy sits between the frontend and WordPress. Its role is not to expose the whole CMS or create an open forwarding service. Its role is to safely forward a small number of approved public routes, such as project listings, work items, blog posts, taxonomies or custom public endpoints.
A good proxy can centralise logging, preserve query strings, enforce allowed paths, cache GET requests, check content types, return consistent JSON errors and give the CMS host a smaller and more stable integration point.
The proxy still needs to be designed carefully. If it uses the same kind of shared infrastructure that the origin is already challenging, it may simply move the problem rather than solve it. In some cases, the better fix is still host-side, such as allowlisting the deployed runtime, using a static egress IP, relaxing bot protection for /wp-json/* or excluding specific public REST routes from CAPTCHA behaviour.
The right answer depends on the project, but the decision should be made deliberately. A proxy is most useful when it gives you clearer control over access, caching, diagnostics and failure handling.
Why browser testing is not enough
A browser test only proves that the endpoint works for that browser, from that network, under those conditions. It does not prove that the deployed application can reach the same endpoint from its production runtime.
A better test checks the endpoint directly in a browser, from local development, from the deployed preview and from the published production site. It compares not only whether the request “worked”, but whether the status code, content type, final URL and body shape are what the application expects.
If preview works but production fails, look at deployment cache, environment variables, route caching and runtime differences. If a browser works but the server-side fetch fails, look at WAF rules, bot protection, IP reputation, request headers and origin security.
Those are different classes of problems. Treating them as the same issue usually wastes time.
What this means for headless WordPress
This is not a reason to avoid headless WordPress. WordPress remains a practical CMS for many projects, particularly where clients need a familiar editorial interface and developers need structured content, custom post types, taxonomies, media handling and established workflows, or have a vast CMS of legacy data.
The point is that headless WordPress is part of a distributed system. Once the frontend is hosted separately, the production route between the frontend and the CMS becomes part of the architecture.
The question is no longer just “does the API work?” A better question is “can the deployed frontend reliably fetch the CMS data through the real production path?”
That includes DNS, SSL, CORS, server-side rendering, cache behaviour, environment variables, WAF rules, bot protection, hosting provider limits and support processes.
This is where many projects fall into a gap. The frontend developer sees what looks like a CMS failure but the CMS host sees suspicious automated traffic. The frontend platform sees an upstream response problem, yet the client sees missing content. Nobody is necessarily wrong, but nobody owns the whole path unless the architecture has been considered properly.
What I would now include in a headless CMS build
For any headless WordPress project using SSR, I would treat several checks as baseline rather than optional.
The build should include server-side fetch logging, content-type checks before JSON parsing, clear error states, short caching for public CMS data, environment-based CMS URLs, live runtime API checks and a documented fallback plan if the origin blocks requests.
It should also include a support-ready way to prove what is happening. If the CMS host needs evidence, the developer should be able to provide the response status, headers, content type, final URL and a short preview of the response body from the deployed runtime.
This might sound like extra work, but it is far less work than diagnosing a blank live site after launch.
It also makes AI-assisted development safer. If an AI tool builds the frontend integration, the developer still needs to know where the code runs, who makes the request, what the CMS host sees, what happens if the response is not JSON and where the useful logs are.
Those are not old-fashioned concerns. They are becoming more important as web stacks become easier to assemble.
The wider lesson
Modern web development is increasingly composable. A project might combine WordPress, a modern SSR frontend, AI-assisted development, edge hosting, CDN caching and automated security tooling. That is powerful, but every boundary between systems creates another place where assumptions can fail.
The frontend assumes the CMS will return JSON. The CMS assumes the request is legitimate. The host assumes suspicious traffic should be challenged. The developer assumes browser testing proves the endpoint works. The client assumes that if the CMS has content, the live site will show it.
The hidden DevOps problem sits between those assumptions.
It is not the most visible part of a project and it is not usually the part people talk about in demos, but it can be the difference between a headless build that works in theory and one that works reliably in production.
Headless WordPress is still a strong option and SSR is still valuable, see my post on the benefits of using SSR for SEO. AI-assisted app builders are still useful, but it’s the integration path that matters.
The CMS, frontend host and security layer need to be treated as one system, not three separate pieces that happen to talk to each other.
The code can be right and the content can be correct, but the live site still needs permission to read it.
If you're planning an MVP or early-stage product and want to make sure the foundations are right before you build, we're happy to talk it through.
Start a conversation