Next.JS 3.0 & Headless Wordpress in Production: A Deep Review

My favorite nonprofit site gets a makeover and performance boost

World Central Kitchen’s new website, built with Next.js.

My big thing now is the headless CMS— and with the Wordpress REST API recently merged into the core, it’s entirely possible to ditch Wordpress themes and transform a legacy Wordpress site built with HTML, CSS, and PHP into a Single Page Application (SPA) using the latest JavaScript goodness. One of the essential problems to solve here is how to preserve SEO. While Google has began to pay attention to indexing SPAs, it’s not yet up to par for the average eCommerce website with potentially thousands of posts and products. Next.js, the minimalist framework from the folks at Zeit, solves this problem and a whole lot more!

Why Go Headless?

Before I go into a technical discussion about how I built this, lemme ask the basic question of why anyone would want to build a “headless” website?

A “headless” site basically means the front-end is decoupled from the back-end, and the back-end (in this case, Wordpress) is serving content through the API rather than handling the front-end logic. In a headless setup, you basically scrap the native theming engine and bring your own technology to handle the build.

Let me play devil’s advocate against my own idea here: With Wordpress, you get a lot of free stuff that you won’t get in a headless/decoupled setup: pre-built themes, plugins for front-end features, an entire ecosystem of solutions to solve problems since native Wordpress has been around for a long time. Headless sites are a relatively new territory and hence, all of this stuff you get for free with Wordpress goes out the window. Search widget? Forget about it, you have to roll your own.

Sounds terrible, but actually it means your site will be much lighter weight than the average Wordpress site and you can use any technology you want to build it. The organization of a React or Angular site is also better than that of a PHP-based Wordpress site. You’re going to get a better separation of concerns and ultimately, more security, since the back-end is only being interacted with through the API and not serving the front-end.

Without all of those plugins, and with your front-end features being rolled from scratch, you can get better performance and security since you will be less dependent on shady plugins from third parties.

Ultimately, going headless with your CMS-powered sites means you’ll become a better developer. Nobody wants to be locked in to a vendor-specific skill set either.

Universal Web Applications

Whatever you want to call it, JavaScript rendered on the server has been a difficult problem to solve for a lot of developers. With PHP, this wasn’t something we had to think about — because it happens by default. Aside from actually rendering server-side partials using something like EJS, React is your best bet for “out of the box” server-side rendering. You don’t even want to look at Angular 2/4/6/10 (or whatever) Universal…

In a previous version of Dine-N-Dash, an event website I built earlier this year, I used React.renderToString to render the static content on the server as beautiful HTML — not a hard thing to do. One big problem though: what about async? What about all that content being fetched from Wordpress? Without some kind of complicated wiring (Redux on the server?), the server won’t render any of that content as HTML. You’re left with just whatever hard-coded content there is in your components and nothing dynamic being rendered.

Next.js solves this problem in a quite beautiful way.

The function getInitialProps is basically your ticket to asynchronously rendering fetched content on the server before it hits the client. You’ll see here some other fun stuff like Async/Await and it’s using isomorphic fetch to make those calls smaller and easier to read. I have some of my components doing three different fetches on the server before rendering and it still doesn’t seem slow!

What’s being returned are new props… in this case, and this.props.recentPosts for one of my single page templates. You’ll also notice it accepts a query param for parameterized routes. I am using the library Next-Routes to make this a little easier, but I do believe there’s a way to do it out of the box.

Unparalleled Simplicity

The really cool thing about Next is it handles all the rubbish you normally have to deal with in building a React website — Webpack, Hot Module Reloading (HMR), routing, the server (mostly). Out of the box, you can simply have two folders: pages and components, pages being your routes and components being whatever else, and it’ll handle the rest for you! You don’t even need a server for a very simple site. In my case, the custom server looks like this, because I wanted a custom port and to use parameterized routes…

That’s all. Not much to it. Even my Grandma could build a Next site…

One trick I figured out, and I was performance hacking like crazy on this site, so obviously I enabled gzip compression. While the text assets/scripts linked to the page will compress, the page itself won’t gzip since it’s being SSRed and doesn’t really have a content type — you have to add the ‘text/html’ header for NGINX to recognize and gzip it. Easy cheesy.

With server-side rendering, it’s a good idea to keep your other server functions (like fetching, caching, authenticating, ect) in a separate Node instance. Don’t commingle your API routes, for example, with your SSR logic. Node is single-threaded, and SSR is a CPU-heavy task. You’ll maximize your CPU if you keep them separate.

Likewise, if you’re using Nodemon or something similar in development for the server, it’s not going to work with Next since it has its own HMR handling. Just keep the Next.js server clean and single purpose, and make a separate Node server for everything else (if you need to — you might not!). Use an NPM script to start both at the same time.

NPM RUN DEV (for development with HMR and with a custom server…):

"dev": "node server.js & nodemon ./api_server/server.js

NPM START (for production):

"start": "NODE_ENV=production node ./server.js & NODE_ENV=production node ./api_server/server.js",

Pages and Components in Next

All of my pages are wrapped in a Layout component, which also contains the meta tags displayed on each page and includes things like the navigation, which is used on every page. This is also out of the box with Next: the ability to set meta tags using a Head component… My homepage looks like this (sans the imports for brevity), and I’m feeding some props to that Layout component for the Meta tags, if you were curious…

It’s just standard React for the most part. Next is very much a minimalist framework, and the things it handles are the things that make React a chore. It lets React be React, and takes care of everything else.

Styled JSX

Next supports a lot of different ways to do CSS, and as we know there are all kinds of opinions about preprocessors. Honestly, there’s a lot of decision fatigue in this space right now. Though I usually prefer Sass, I decided to give the packaged method of handling CSS a try, Styled JSX — another awesome tool from the Zeit peeps that comes with Next, you don’t even have to NPM install it.

Styled JSX is the familiar inline “CSS in JavaScript” that you’re used to with React, allowing you to have local styles that don’t cascade, but it handles the server-side rendering of those styles for you. Super nice! Even with JavaScript disabled, your site will be mostly intact when it initially renders… It also adds a lot of the browser vendor prefixes automatically so you don’t have to think about them.

As I was focused a lot on performance for this site, I have learned that you really should inline all of your CSS — and localize it as much as possible to the component— if you want to speed up your site.

CSS render time is actually a huge issue if you use something like Bootstrap, which I avoided here. The only thing I did use was the Bootstrap 4 grid (just the grid), which I’m used to — I pulled out the individual grid and removed basically all of the styles I don’t use. This was just a convenience move. I included the grid as inlined CSS on the layout component. It works fine… In the future, I’ll use the very rad and new CSS Grid standard to avoid having to mess around with this at all.

One thing I noticed with inlined CSS, as this was the first time I’ve used it, is it sometimes doesn’t work properly on some content, like what’s set with “dangerouslySetInnerHtml” — which is pretty critical to use since Wordpress markup is stored as HTML. Having the styles inlined can lead to to some inconsistent rendering since that content isn’t part of the same DOM nodes as the parent component (my theory, at least). Styled JSX provides a “global” option where you can embed things like your grid, fonts, Wordpress gallery styles and basically things you will need on every page — this solves most of the problems with inlining so it acts like normal CSS.

The Quirky Wordpress REST API

Some may ask, why Wordpress — why not Contentful or Prismic or one of the new cool, truly headless kids on the block (lol meme this please)? Well, when you’re dealing with a 5 year old site, there’s a lot of old content to migrate. Maybe we’ll eventually get there… Wordpress does what we need it to do for the most part and it’s free, and my end users are not developers (they’re fundraisers/comms folks), so they are familiar with Wordpress.

This year, Wordpress integrated it’s REST API into the core, and with a few plugins, you can do basically anything you want with it. The devil is in the details. Ironically, it’s dealing with the “tried and true” here that was the biggest problem, not the friendly and lovable Next.js.

Handling Routes Effectively — And Keeping Old URLs Intact

One of the interesting problems with Wordpress is the REST API doesn’t offer the same magic tricks as the native PHP API. So for most of us, if we have a page at and a post at, Wordpress internally sorts out what template to use based on the post type. But with the REST API, you don’t get this sort of magic. You have to know in advance whether it’s a post or a page. Hence, a parameterized route with is not going to tell you which API endpoint to go for.

Enter the WP Multiple Post Type plugin, which can basically let you query the API for any pre-set content you choose in one request. The Express middleware for my “all-in-one” posts/page route is below. Note that I’m using Redis and Bluebird Promises here, I’ll talk about that in a bit. Then on the front-end I can display things like the date or the subheadline depending on the post type.

Note the “new PostOrPage(content.body[0])” class, that’s where I’m filtering through and manipulating the response for the front-end before I send it up. With SSR, your async isn’t just being held in memory… the entire object is returned as statically rendered script content. You don’t want to return a bunch of cruft to the front-end you’re not using as it’ll increase the page download size… This is what I call a labor of love.

Featured Images

Featured Images were actually a big problem with the REST API. Not only are they deeply nested in a cluster of an object/array, but depending on what the user uploads, the feature image may or may not exist…

The problem comes server-side. All of my Wordpress REST routes are being served by an intermediary Express server. This is so I can cache them in Redis, but also to trim down the response so it’s not so verbose. I setup an ES6 Class that is used to filter the very verbose response from Wordpress to send up something easier to work with on the frontend. I guess I now appreciate GraphQL — lol.

The problem: If the user uploads a featured image smaller than the crop size, Wordpress doesn’t return a “null” response or some kind of default in its place. The object — and even the array — simply doesn’t exist. When you’re dealing with arrays nested in objects, this can lead a catastrophic error where the request completely fails versus returning a null for the specific field. So you have to test for it first. Whata pain.

This is also where I actually had to do some PHP to add new featured images to the Functions.php file…

Advanced Custom Fields

Advanced Custom Fields are the magic that makes Wordpress basically a CMS for any kind of website and not just a blogging engine. I generally use Advanced Custom Fields and Custom Post Types all over the site — from the Chef Network Bios to the whole Projects category… Even the slider images are implemented as custom fields. So basically, this stuff needs to be returned by the REST API. There’s a very simple plugin that does this for you. Ironically, the custom field content is way easier to deal with than the native Wordpress content.


We were originally using the All-In-One SEO Pack to handle things like meta tags and social sharing images across the site, but those fields aren’t returned to the REST API. I switched us to Yoast, which has a way of converting all that content over, and there’s a nice plugin you can download to expose the fields to the REST API!

Handling Drafts

Never quite figured out previewing drafts in a headless setup, so I just used a simple PHP template to handle this. Yes, this sort of breaks the whole notion of it being completely headless. The problem is Wordpress authentication sucks — the WP Admin can’t share the cookie with outside applications, so in order to view a draft on the frontend, I’d need the end user to authenticate again — and that means basically more plumbing and potential security issues for such a small feature. Just use PHP.

Archives and Search

The REST API has very little out of the box in terms of archives. Archives by date are particularly problematic, you basically have to roll your own with separate fetches and loop action.

In our case, archives by date make little sense compared to archives by category. So I rolled my own endpoint to send the latest posts to the front-end, categorized, in one request. Making things easy on myself on the front-end was the goal of separating all this logic into an API server. Then each of these categories link to their own specific archive page. This is good enough for what we need.

Below it is a search box. Search is quite straightforward in the API, you can do it from a single endpoint and it seems pretty fast.

Checkout the Stories page to see this all in action…


Wordpress’ REST API is kind of slow, especially if you have a lot of plugins. Every plugin has to be initialized during the response. As mentioned previously, I decided to use Redis to alleviate some of these issues by caching the API responses. With Redis, I can reduce a 200–500ms response to something like 30–40ms cached. That’s pretty significant and worth it for the extra effort.

I tried one of the Redis plugins for Wordpress and it didn’t really seem to speed up the REST API response, so I decided to roll my own.

One of the eternal problems with caching is how to handle “decaching” — how are you going to devalidate the cache once content is updated on the frontend without relying on end users to press a button?

Enter the Functions.php file, where I wrote a very simple CURL request to hit my decaching endpoint.

This function fires off pretty reliably whenever a page or post is saved, and it resets the cache.

Note, I did save the slug here in case in the future I try to go more granular with my caching. The thing that sucks is when I decache, everything is cleared out… So whoever makes the first hit to that content will get the uncached version.

Being more thoughtful about how those objects are stored would allow me to perhaps replace rather than flush what’s in the cache with the updated content. But that’s a lot of PHP to do right (which I’m mostly trying to avoid with this whole build!). Caching can get really complicated and it’s probably not worth that level of granularity… So this is more a quick and dirty cache.

Caching became less of an issue when I moved into production, cleared out a whole bunch of plugins, enabled gzip and HTTP2— then I was consistently getting < 200ms fetch times, sometimes even less than 100ms uncached. Not bad… When I rebuilt the Dine-N-Dash site for Next, I didn’t use Redis caching at all… it’s still very fast. Maybe I can be called out for premature optimization… But it was an interesting exercise nonetheless.

Performance Outcomes

One of the big challenges with managing corporate websites is the end users. Most marketing people (and I used to be one — they’re great people) just don’t have the time to resize/compress images and mess with the granular details of performance that I as a speed freak developer care about. Hence, some performance details are just outside of your control — and some things, despite their performance hit, will end up on the website… There’s not much you can do.

With this website, I spent a good 70 percent of my time on optimizing performance where I did have control. Every bit of CSS and JavaScript I could remove I did. I use the Lighthouse tool from Google (now a part of DevTools) to measure performance. The old homepage consistently was rated around 45–60. With the new design, I got it here:

Not too shabby. Some pages are a bit faster, some a bit slower. This is an image-heavy website, so it will slow down depending on the images. Nonetheless, it’s a whole lot better now, especially on mobile.

The performance tweaks that made the biggest impact were enabling gzip in NGINX, building features from scratch rather than using premade libraries, shrinking images (like the homepage masthead), inlining CSS, and serving different images to mobile vs. desktop where I could.

I tried using JetPack to serve images from a CDN, but ultimately disabled it. Funny thing, CDNs don’t always speed everything up. In this case, things were slower. The web server is really close to their user base in Washington, D.C. — a CDN probably doesn’t make any sense.

Homepage performance isn’t where the buck stops for this site, but I did focus on getting that score up. One thing I did do was lazy load some of the page content (particularly the YouTube video, since it was a huge drag). It’s something you want to use mindfully because lazyloaded content does not get server-side rendered!

Overall, I’m happy with the performance of the site. It was a big focus here and I learned a lot. There’s always more to do!

Conclusions and Outcomes

Ultimately, Next.js is quickly becoming my go-to framework for dealing with these kinds of builds and I have at least three more projects down the pike, including two commercial eCommerce sites, for which I will use the framework! It’s really exciting what you can do now with JavaScript.

I didn’t get a chance to play with some of the Next 3.0 features (it just was released yesterday!) like static site exports — I’m sure that’s really cool! I did notice that between 2.4.9, which I originally launched this site on, and 3.0 —there was a noticeable performance increase in development and production. Kudos to the Zeit team for building such a solid framework to supercharge our React sites!

You really should check out the site and the org, World Central Kitchen. It’s a great bunch of peeps I’ve known for many years now — using the power of food, social enterprise, and a network of talented/sometimes famous chefs to change lives around the world. What a cool mission! Also, don’t miss Dine-N-Dash next year, June — if you’re around Washington, DC. It’s a fun event. That site is also built with Next. I’m 2 for 2 here! Thanks for reading.