Headless WordPress is Overrated: A Case for The Nearly-Headless Web App
Tags: wordpress, alpinejs, javascript, php
Published: 2021-07-02 14:05:26

Over the last few years, I’ve built a number of fully headless WordPress websites with the REST API and React. I loved how fast the end result is, and if done correctly, how powerful and extend-able you could eventually make page creation. Plus it just _feels nice._ Loading transitions, and the general behavior just makes your website feel fresh, and modern. It provides a polish that even the fastest non-headless sites can’t quite achieve.

I avoid creating headless WordPress websites. It creates a lot of extra overhead, which creates more bugs, and ultimately ends up making the site much harder to maintain. I typically stick to the basics – fast hosting, and aggressive caching.

> Headless WordPress kind-of sucks. You give up _a lot_ of help from WordPress in-search of an app experience. I have found myself frequently dreaming _“what if I could have it both ways?”_

## The Problem With Headless

One problem with fully-headless WordPress is routing. Behind the scenes, WordPress has a lot of logic built-in to handle routing, and with a headless approach you have to build something to handle that on the front end. Ultimately, you’re re-inventing the wheel, and it takes a lot of extra time to build.

Another problem with headless WordPress quickly becomes apparent the moment you try to use most WordPress plugins. The ugly truth is that you usually have to re-invent a lot of things just to get the plugin working properly. For example, try to create and use a [Gravity Forms](https://www.wpdev.academy/recommends/gravity-forms/ "Gravity Forms") form, and then use it in a React app. You can’t really do it without re-building the form’s render, validation, and submission logic on the front-end. That’s _a lot_ of extra overhead to maintain. This example is something as simple as _adding a form to a website_. Imagine how complex things get when you look at things like integrating e-commerce tools, like Easy Digital Downloads, or WooCommerce.

## Re-Thinking “Headless WordPress”

I knew when I committed to upgrading my personal theme, I wanted it to be a fast, and have an app feel, but I didn’t want to completely sacrifice all of the natural capabilities that WordPress plugins offer. This site, for example, uses [LifterLMS](https://www.wpdev.academy/recommends/lifterlms/ "LifterLMS") for its courses, and it would have taken a lot of extra time to rewrite all of those course templates from-scratch.

I also wanted to _use_ this tech for our premium clients at [DesignFrame](https://www.wpdev.academy/recommends/designframe/ "DesignFrame"). Because of that, we needed a way to maximize compatibility with WordPress’s native features, and ensure that plugins remain compatible with whatever we build.

So, with that, here’s the key parameters of this approach:

1.  The theme had to be compatible with other plugins _without re-building a bunch of custom logic in the process_.
2.  Other developers should be able to pick up and work with the theme with a minimal learning curve.
3.  The site needs to just _work_ with WordPress’s block editor without needing any changes to the theme.

These parameters _immediately_ revealed a couple technical truths:

1.  The HTML markup needs to be rendered inside WordPress. This ensures that plugins can render their output in the same way they do with any other theme.
2.  The app needs to rely on WordPress for routing, and needs to handle any custom page from any plugin without fail.

Neither of these things fit the description of headless WordPress.

## Enter Our Nearly Headless Web App

I like to call this a _nearly headless app._ Partially because it makes sense – the app still relies on the server to get started, but once the server provides the initial load, the app can usually take it from there. But let’s be real, I just wanted an excuse to put John Cleese in my blog post.

<div class="wp-block-image">

<figure class="aligncenter size-large">![](https://www.wpdev.academy/wp-content/uploads/2021/07/tumblr_inline_nb93aw3mNs1rmc5y8540.gif)</figure>

</div>

Here’s the gist:

1.  The system uses [AlpineJS](https://github.com/alpinejs/alpine) for rendering. It’s light, fairly easy to understand, and it plays exceptionally nice with PHP server-side rendering.
2.  Most of the theme is is loaded around HTML [template](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) tags. These tags get populated by WordPress’s REST responses for post content.
3.  The system makes judicious use of [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). This drastically reduces the number of REST API calls, and keeps the site running fast.

### How Nearly Headless WordPress Works

The site is loaded just like any regular ol’ WordPress site. The key difference is “the loop” is replaced by a template tag, which uses Alpine’s `x:forEach` loop to actually render the loop. It looks something like this:

<pre class="snippet line-numbers language-php">	`<span class="token php language-php"><span class="token delimiter important"><?php</span>
<span class="token comment">/**
 * Index Loop Template
 *
 * @author: Alex Standiford
 * @date  : 12/21/19
 * @var $template \Theme\Templates\Index
 */</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token operator">!</span> <span class="token function">theme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">templates</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">is_valid_template</span><span class="token punctuation">(</span> <span class="token variable">$template</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span>
	<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token delimiter important">?></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loop<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span> <span class="token attr-name">x-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>( post, index ) in posts<span class="token punctuation">"</span></span> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>index<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
			<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span> <span class="token attr-name">x-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">'</span>post<span class="token punctuation">'</span> === getParam(index, <span class="token punctuation">'</span>type<span class="token punctuation">'</span>)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
				<span class="token php language-php"><span class="token delimiter important"><?=</span> <span class="token function">theme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">templates</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">get_template</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'post'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'post'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token delimiter important">?></span></span>
			<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span>
			<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span>
				<span class="token attr-name">x-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">'</span>page<span class="token punctuation">'</span> === getParam(index, <span class="token punctuation">'</span>type<span class="token punctuation">'</span>) || <span class="token punctuation">'</span>course<span class="token punctuation">'</span> === getParam(index, <span class="token punctuation">'</span>type<span class="token punctuation">'</span>) || <span class="token punctuation">'</span>lesson<span class="token punctuation">'</span> === getParam(index, <span class="token punctuation">'</span>type<span class="token punctuation">'</span>)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
				<span class="token php language-php"><span class="token delimiter important"><?=</span> <span class="token function">theme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">templates</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">get_template</span><span class="token punctuation">(</span> <span class="token string single-quoted-string">'page'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'page'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token delimiter important">?></span></span>
			<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span>
		<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
	<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span>`
</pre>

I’m using [Underpin’s template system](https://github.com/Underpin-WP/underpin#template-system-trait) in the example above, but you could just as easily do this with `get_template_part()` instead of `get_template()`.

Once the page loads, AlpineJS fires up, and renders the content using the REST API. Since the initial endpoint is preloaded, it grabs the data from the cache, loops through the content, and renders the result. The REST response is also saved in session storage (more on that later).

Behind the scenes, the app scans the entire rendered page for internal site links, gathers them up, and sends them to a custom REST endpoint. This endpoint takes the URLs, retrieves the post object associated with each one, and returns them to the app. The app takes these objects and puts them in session storage for later use.

When a link is clicked, the app intercepts the event, and checks to see if the post for that link is stored in session storage. If it is, it re-renders the page using the data from session storage, and pushes the URL to the browser’s history. If the page content isn’t stored in session storage, it simply loads the link using the default behavior.

## Key Benefits

### Your Website Works Less

Because most of the content ultimately gets loaded from the session storage, the app has all of the information needed to render content _without contacting the server_. This takes a lot of strain off of the server by reducing the number of requests a visitor makes when exploring your website. Not only does this mean your site will run faster, it also means your site will be able to handle more concurrent visitors without slowing down.

### Faster Experience On Slow Networks

Another benefit of the nearly headless WordPress app is how much better this app performs on a slow network. I spend [a lot of time in the boonies](https://casualweirdness.life/), so I’m painfully aware of how much an optimized website can improve a person’s experience. The initial load won’t be any better than a normal site, but when it loads, the rest of the content is fetched in another request. Once that loads, the site will load instantly, even though the network is slow.

In fact, in testing, I was able to load the initial page, turn on my iPhone’s airplane mode, and still navigate most of the site as-if I had a lightning fast connection.

<figure class="wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter">

<div class="wp-block-embed__wrapper">

> .[@DFS_Web](https://twitter.com/DFS_Web?ref_src=twsrc%5Etfw)’s website redesign will make it possible to visit any page without an internet connection shortly after the first page is loaded. This makes this site FAST even if your internet connection is slow. [pic.twitter.com/keOxyMU8cq](https://t.co/keOxyMU8cq)
> 
> — Alex Standiford (@AlexStandiford) [December 9, 2020](https://twitter.com/AlexStandiford/status/1336468966160683017?ref_src=twsrc%5Etfw)

</div>

</figure>

### Problem Pages Can Bypass the App

The app only instant-loads if the content is in the session storage. This means that you can “disable” the app on pages that, for whatever reason, need to run through a WordPress request when visited. This theme includes settings page that makes it possible to add a list of pages to explicitly force to load in this manner.

This makes it possible to fallback to a more-traditional theme load on pages that somehow conflict with the app. That gives us a way to quickly fix pages that are behaving unexpectedly without needing to make any immediate changes to the theme.

This allows me to quickly put a quick-fix to problems that crop up, and then implement the necessary upgrades to the theme to fix the conflicts and re-activate the app on that page.

This also allows us to completely disable headless WordPress when it’s convenient from a technical standpoint. Some pages would require _a lot_ of extra work to re-build using REST. For example, a cart page on a website that uses an e-commerce solution would require a significant rewrite of the template because these plugins expect a traditional request to occur when the page is visited.

## Caveats

This system avoids _most_ problems that headless apps create, however, in its current form, it has a few gotchas. Fortunately, these issues have been relatively easy to fix, and can often be avoided altogether by simply disabling the app for the pages that are impacted.

Scripts and styles are probably the biggest headache that comes with any method that renders with Javascript, and this system is no exception. Any plugin that enqueues a custom script or style on the front end _will not work_ if the page is loaded with the cache. This is because most plugins only load scripts and styles on pages that _need_ the scripts. This can usually be avoided by forcing any page that utilizes these plugins to load without the cache. That will force the site to load the site normally, which usually makes everything work as-expected.

In my build, Gravity Forms _still_ didn’t work, even when the page with the form was loaded normally. This was because Gravity Form’s script fired _before_ Alpine rendered the content. This caused Gravity Form to fail.

To get around this, I had two options:

1.  Force all pages that have a Gravity Form to load without Alpine, using a traditional loop. Easy, but not as nice.
2.  Modify how Gravity Forms renders its forms to use Alpine + Gravity Forms REST API. Harder, but nicer.

For this specific problem, I opted to spend a few hours getting Gravity Forms working with Alpine. Since I’m still using WordPress to do my rendering, I didn’t have to re-do the rendering portion, just had to modify it a little to use Alpine’s event handles. This ended up being significantly easier than what I’ve had to-do in React, where I had to also re-create the forms in JSX. I didn’t have to re-invent the rendering, just had to get _submissions working_, and it took a lot less effort to achieve.

## Conclusion

When I started working on this, I knew that there would be a little more overhead than a basic theme, and a lot less overhead than a site builder. The goal, however, was to minimize the overhead enough to make it a plausible option for our premium clients. This approach offers a _lot_ of performance gains without adding a lot of extra overhead in the process, and most-importantly, provides a traditional-loading safety net for pages that are being naughty.


## Looking for more WordPress Resources?
Join WP [Dev Academy’s Discord server](https://discord.gg/ArqzZmqfYZ), and become a part of a growing community of WordPress developers.