How to Build Your First Headless WordPress Project with ACF + WPGraphQL

Everyone has their favorite tools when it comes to WordPress development, and the idea of building a headless site without those tools can be daunting. Luckily, two favorite tools of WordPress developers—ACF and WPGraphQL—can help you dip your toes into headless development.

In this session, WP Engine Senior Developer Advocate Jeff Everhart shows off the ins-and-outs of the infrastructure by building a demo site in just minutes using Atlas!

Video: How to build your first headless WordPress project with ACF + WPGraphQL

Session Slides:


JEFF EVERHART: What’s up, everyone? My name is Jeff Everhart, a senior developer advocate here at WP Engine. Thank you so much for joining my talk today on building your first headless WordPress project with ACF and WP GraphQL. Before jumping into building, I always like to give some context on what headless WordPress is. It seems like headless is becoming less of a niche term and more mainstream, so I’ll try to keep the explanation short.

Headless WordPress at its core is about using the WordPress CMS through an API to power some other type of application. If we look at the diagram in this slide, when a user requests, instead of having WordPress handle that request, a JavaScript application responds to that route. It looks at the path, determine what data it needs from WordPress or other sources, makes those APIs calls, and generates a response to the user.

It’s important to note that what’s in that Node.js runtime slot could really be any number of different types of applications or clients. We’ve seen various examples of mobile apps or websites built with all of the most popular frameworks use this pattern with great success. Now that you have a good idea of what headless WordPress is, let’s walk through the tech stack that we’re going to use for today’s project.

At our back most layer, we’ll use WP GraphQL as our API. WP GraphQL turns your WordPress backend into a powerful GraphQL API. It enforces all of the same role and capability security checks that native WordPress and the core REST API do. WP GraphQL is a great choice for developing headless applications because using its declarative query language, we can query for different content types all across that content craft. So getting posts, pages with their categories all in the same request.

Now, let’s see how we can structure and model some more complex data using the WordPress CMS. Many devs approaching headless projects need to model and store data that is different, right? Extends beyond WordPress’ typical posts or pages. ACF or advanced custom fields is a great tool for modeling those custom fields. In an upcoming release, it will become even more powerful by allowing you to register post types and taxonomies directly in the UI in the free version of this plugin.

It makes all of the data available over the REST API easily but will need to install a WP GraphQL extension to add some of that field data to our GraphQL schema. Personally, I’m really excited to see this plugin evolve as it becomes more powerful for headless developers.

Now, that we can model complex data and query it through WP GraphQL, we need some way to create those modern web experiences for our users while also supporting the content editors that already know and love WordPress. That’s where Faust comes in. Faust is a two-part framework consisting of a WordPress plugin and a Next.js based JavaScript framework. They work together to make building headless WordPress sites easy and intuitive.

It adds support for things like post previews and authentication out of the box, and you can lean into a re-imagined WP template hierarchy experience in modern JavaScript. Faust.js aims to be a very extensible platform and ships with a plug-in system and an admin bar that you can use to customize and create experiences that support your particular use case.

Last but not least, we’ll need some ways to work with these tools locally and in production. Using local and WP migrate, I’ve pulled all of the WordPress resources you’re going to need for this project into a single zip file that you can drag and drop into local to get started. Once we’re done building our site, we can push our work to the Atlas platform. Atlas WP Engine’s all in one headless hosting solution that combines your WordPress backend and a Node.js container all tuned to work together available through one seamless dashboard.

So now that you have a good understanding of all the tools that we’ll be using in today’s presentation, let’s jump right in and start building. Open the URL in this slide to access the demo code in the browser. I’ve tried to make this experience as easy as I can to follow along with, but we’ll be covering a lot of ground in the next 25 minutes, so feel free to just watch now and refer back to this GitHub repository and the recording of this session later to keep working after the conference. If the rest of decode is a car show, then this session is on building engines. Pun totally intended.

Now that we have the GitHub repository open, let’s get started. The first thing I recommend you do is make a fork of this repository in your own account. If you look through what’s contained in this repository, you’ll see a bunch of sample code as well as some step by step instructions in the readme for everything that we’ll do in this presentation. In fact, we’ll do a lot of copying and pasting directly from this repository into your project. To clone this down locally, let’s go ahead and run the Git clone command inside of your terminal.

That’ll take just a second to download, and then once we’ve downloaded the project, let’s go ahead and change our directory into that project directory. From there, I’m going to run a command to open this project in VS Code, but you should feel free to use whatever code editor you want. Since VS Code has an integrated terminal already, I can clean this up and then just go ahead and close out iTerm. And now, we can transition to getting our WordPress local site situated. To do that, we’re going to open this project in Finder and then locate the headless WP demo zip file that I’ve prepared for you.

We’ll drag and drop that zip file directly into the local development environment, and local will start the process of unpacking and bootstrapping your demo WordPress site for you. We can pretty much stick with the defaults, and then local will take just a few minutes to create you a new WordPress site. Now, this is a great feature of WP migrate pro that allows me to export any site as a zip and drag and drop that directly into local so that I can take a production site to my local machine very quickly, regardless of the platform.

Once that’s finished installing, you may want to go ahead and trust this SSL certificate, and then we’ll go ahead and open this project up in WP Admin. From there, we’ll go ahead and just minimize local, since it’s already running, and we don’t really need to do anything else with it. So from there, we’ll open up our repository, where you’ll find an admin username and password that I’ve already created for this demo site. You should be able to use those credentials to go ahead and log in to the WP Admin dashboard. From there, we’ll take just a second to get situated with this local WordPress install.

The first thing that we’ll do is take a look at the plugins that are installed with this WordPress site. We see things like advanced custom fields for data modeling. We have fouls to enable some of our headless capabilities, WP GraphQL to act as an API for our site, and then WP GraphQL for ACF extension to surface some of the ACF field groups. Now, let’s take a look at the posts as well. If you look at all of the different demo posts that I’ve pre-populated inside of this project, we can see we have a number of different posts with a bunch of different categories.

If we take a look at the content inside of each post, we’ll see that we’ve got a bunch of body content, some images, featured images, and some ACF field groups that we’ve already created and pre-populated. So let’s take a look at the field groups in more detail. If we open up the ACF menu, you’ll see we have two field groups created– food resources and sweet mixtape. Let’s take a look at food resources first.

This is just a really basic field group with two individual fields– text and URL. For each one of these things, I have the Show in GraphQL option checked so that they appear in GraphQL. Then I also have checked that option at the field group level. In addition, I have some conditional logic to determine when to render these posts– so if the post category is equal to food. And let’s check out what sweet mixtape looks like.

We’ll see that this looks pretty similar to our food resources, and that we have a couple of different fields. Each one has the Show in GraphQL option checked, as well as having that checked at the field group level in addition. We can see that there are a couple of different settings that the WP GraphQL extension gives us that we may not need. In addition, we can see that we’re conditionally displaying this based on post category as well.

So now, let’s take a look at WP GraphQL and what that gives us in terms of tools. If we open up the GraphQL menu, we’ll get dropped inside of the graphical IDE. Now, this is an interactive development environment that allows you to create queries using WP GraphQL. We can specify that we want, say, the first 10 posts with categories and include our additional ACF fields on here as well. When we click the Run button, we get back live data from our WordPress site that matches the data in that query.

If we want, we can open up the Query Composer window and visually compose aspects of our query. So this is a really handy tool if you’re not sure what particular fields or data might be on a particular WordPress object. You can use the Query Composer to explore and run those queries in real time. Now, let’s move to configuring Faust by opening up the Faust settings menu. As I said in the intro talk, Faust is really a two-part framework that consists of a WordPress plugin and your front-end code base.

So if we come back into the WordPress plug-in settings menu, we can see that we’ve set this option for front-end site URL, which is going to point to the address of our front-end site. Right below that in the secret key option, we’re going to go ahead and click regenerate to generate a unique key that you can use for your demo project. From there, let’s actually hop back into the JavaScript code base, and we’re going to run this command to copy a sample environment variable file into one we can use with our site.

Once we’ve run that, command open up the .env.localfile, and we’re going to make a couple of changes. The first thing is that will replace the next public WordPress URL option with the location of your local site. So ACF.WPEngine.local. And then we’ll also take that secret key value and use that for our Faust secret key. Make sure we want to uncomment that as well, and then that last environment variable is really just useful for testing and local development so that you can smooth out any SSL issues you may have while connecting to the local development environment.

From there, we’re going to want to run NPM Install to install all of our project’s dependencies, and then once that’s done, we can kick this off and run NPM Run Dev to start our project. That’ll take just a second to compile, but once it does, we can open localhost:3000 in the browser, and we should see our Faust site in action.

Once that renders, you can see that Faust is already handling some magic for us. If we look at that menu in the top right hand, we can see that it’s already pulling in pages and content from our menus that we’ve defined in our WordPress backend, and if we hop back into our WordPress Admin, we can look at how this connection works in a little bit more detail.

Because the Faust plugin knows the address of our front-end URL, it rewrites a lot of the links, such as those preview links so that when you open them in the browser, they open in the front-end site URL, so that you’re getting a real live preview of what your content is going to look like on the front-end.

Now, let’s take a deeper dive into the structure of our Faust JavaScript project. If you’re familiar with working in Next.js, you’ve probably used the page’s directory to create page components that implement your routes. You can still do this in Faust, but it builds on this concept by providing a catchall route called the WordPress node that allows you to take any URI that WordPress manages and resolve that to a particular piece of content.

We then get some additional data about that content item and pass that down through our components so that we can resolve that one piece of individual content to a particular template in the WP templates folder. This is very similar to the template hierarchy concept and traditional WordPress theming, and lots of the naming conventions. Match for example, frontpage.js is the front page of our website, while page.js is responsible for rendering things of the page content type. And single.js is there to render single posts.

Now, let’s start and make our frontpage.js just a little bit more dynamic. OK . So to get started, we’ll go ahead and open up our front page.js file and give ourselves just a little bit more room to work. So if we look at the contents of this file, we can see there are three main pieces. There’s the component itself that we render, a query property that gets attached to the component, and this gets run every time we render the component, and then some variables that we can pass in at the bottom.

And as you can see, there are two really main ways to use this. Either using the use query hook inside of the component, or it can get passed down as props into the component itself, and you’ll see that in an example later. So let’s hop back out to the repository and get started with step 2.1 to update the query for our frontpage.js. So we’ll copy that, then head back into the graphical IDE and just play around there for a second. So this query that we have on our clipboard should get the first 10 posts and get a couple of pieces of data associated with each one of those posts.

And so if we click in there and run it, we see that all works great. And so we’ll want to go ahead and add that to our component query property. So we’ll find a good place to do that and just paste that in there, and do a little bit of reformatting. And so what that does is that’s adding this individual part of the query to that query itself. So we still need to make the results of that query available to the rest of our component, so we’ll go ahead and add in this additional line that just extracts that post result into a variable that we can work with.

Now, the next step for making this homepage dynamic is actually creating a component to render those post excerpts. So we’re going to do that and create a couple of files in the components folder, and I’ll mention here that– I’m going to go ahead and create a postexcerpt.js inside of a post excerpt folder, and I’m really just duplicating the structure of the existing components that are a part of this Faust.js Getting Started project. You’re really free to do whatever you want here, and I’m just following the framework as it’s already been laid out for me as a part of this Getting Started.

So once we have all three of those files, we’ll start adding some stuff to them to render those posts. So the first thing that we’ll do is put some content in our post excerpt component. And so we’ll just copy and paste that from our repository, and we can see that we’re importing that module.css file. And we have a function called post excerpt that is our component that tastes one prop that is the post, then we’re rendering a section, having a link that’s going to go directly to that post URI, rendering the title. Then down there, we’ve got category tiles that we’re rendering out as well, and then using dangerously set in our HTML to set the HTML content for the post excerpt.

Now, once all that’s great and saved, we’re going to save that out, but we’ll also come in here and add this additional scoped style to our component to style those span tags for our categories, and then we’ll just do some importing exporting inside of index.js file to take that from a default export to a named export, and we’ll save all of those things. And then the last step to make this available for use in other things is to add one more export line to the index.js file of our components folder. And now that we’ve done that, if we head back into the home page, frontpage.js, we should be able to just add an additional import to our existing import statement to use our post excerpt.

Now, we’re going to want to find a place to implement that, and if we come down and look inside of our main element, see that we have some stuff right below the hero– what we’ll do is we’ll just copy and paste some of the code in the repository and overwrite what’s inside of that main with the use of our post excerpt. And we’ll just do a little bit of reformatting to smoosh some stuff around, but you can see there that what we’re doing is inside of that container, we are going to map through the array of posts that we got back as a result of our query and then return a post excerpt component where we pass in the post and give it a key.

And then if we go ahead and save all of that and render it in the browser and refresh, we should see that we’ve got a great dynamic home page. Yeah, each one of those titles has a clickable link, as do the individual category tiles. And if we click through, we can see that through the benefit of the templates that already exist in Faust, all of our post stuff is already getting rendered, although we’re missing some of those resource groups that we created using ACF. So if we click through to a second post, we can see that one renders great as well as do all of the pre-made category links that are– we’re just using those category URIs to lean into Faust in rendering that category JS template.

Ok now that we have a dynamic home page, let’s move on to getting those ACF fields to render on our single.js template. So go ahead and just clean up our code editor, and then we can open up the single.js file and take a look at what’s inside there. We know that at that top level, we have this component function that we’re exporting that actually takes props, and the component.query property that has a dynamic GraphQL query that’s reading some of those variables that we get back from the seed query.

And what we’ll ultimately want to do in the end is display some of those different post resources at the bottom of our post content. So I head back into the repository and scroll down to step 3.1, where we update the single post query, and we’ll take a look at that because it starts to pull in both the sweet mixtape and food resource field groups that we created in the earlier step. If I take that query and copy and paste it into graphical, I’ll go ahead and hard code a database ID for a post, and remove just a couple of those different things that we don’t need like As Preview and that other fragment.

If I go ahead and run that, we’ll see that I get back some data that contains really what I’d expect. The content, I get back the author, and I get back, importantly, both of those field groups and their data. So I’ll go ahead and copy that query and head back into single.js. From there, I’ll really just replace that part of the query with what I’ve got from my clipboard. We can just go ahead and save that. You can reformat it if you want to, but this is whitespace agnostic, so I think that for the most part that looks just fine.

So from there, we’ll want to do the same thing we did in the last step, where we’ve made that a part of our query. Now, we want to make sure that we make those variables available inside of our component. So we’ll add those to that structuring statement, and then additionally, since we have a couple of different categories for our posts, we want to create some Boolean stuff to help us determine whether or not we should display food resources or sweet mixtape. Because as you can see there, no matter what we’re getting back both, and if there is nothing to find for sweet mixtape fields, those come back as null. So I’ll want to do some conditional checking there, so I’ll add these two lines of code to our file.

And what this basically does is it creates us some Boolean variables that we can use down below to conditionally render templates. In this thing, we’re looking at the nodes for each categories and then basically filtering them to determine whether or not they have food in them or music, and then checking the length. There’s probably a lot of ways that you could implement this type of Boolean variable, so feel free to change this if you have a cleaner way to do that, but for our purposes here, I think that that’s going to work just fine.

Now, the next step is that we’ll actually want to, like in the previous step, create some additional components. So I’ll go ahead and inside of my components file go ahead and create a food resources component. So I’ll create a folder and then a food resources.js file inside of that, and along with that, I’ll create an index.js file as well as a CSS modules file. And now that SCSS modules file is really helpful because it allows us to scope our styles to that particular component. So it feels like funky syntax to do it, but in the end it’s a really clean result because we don’t have to write a bunch of classes and stuff.

So I’ll start copying and pasting the component code from the repository directly into those files. We can see that we’ve got a food resources function that takes two props recipe name and recipe link, then we’re rendering those out below our nice age with the burrito emoji. We’ll also copy and paste some SCC file styles into this particular folder, and then we’ll make sure that we export that from the component folders index.js. And just like in the previous example, we’ll want to go ahead and export this in the index.js of our components folder as well so that we can have a really nice group to import, like you’ve seen directly from that components folder elsewhere in some of these different files.

So once we’ve got that added, we’ll turn our attention to the music resources component, and we’ll do the same thing. We’re going to go ahead and create that same file structure. So music resources folder and a musicresources.js file inside of that. Then we’ll go ahead and create an index.js file to export it and then also our musicresources.module.scss file as well for those scope styles.

So once we’ve got all that stuff, we’ll do the same thing we did for our food resources and just copy and paste some of this code from the repository. We can see that this component looks almost identical. We’ve got music resources. This actually has three props instead of two, but we had three fields on this field group, but the styles.component convention is the same. And we’re just doing some slightly different rendering because we have different content.

So from there, we’ll also add in our SCSS code and make sure to export that from the component folders index.js and then also import it into our folder over here, and I think I actually made– let’s really quickly rename this and just make sure that all of our naming looks good, so that our index.js and components finds our music resources and all that stuff’s great. So now, we’ll go ahead and just close out all of these extraneous tabs because we are ready to actually implement those components in our single.js file.

So to do that, we’ll just add those two components to our existing import statement, which already automatically recognizes those, and we’ll find a good place to do that. And so we’ve got this content wrapper component down here. And so right now, we’re passing in the content, but that actually will also optionally accept children. So we can pass in the content but then also pass in some child components directly to that content wrapper, so that it gets displayed in that nice horizontal space that we already have and everything is in alignment.

So that’s what we’ll do, and then we’ll sort of copy and paste that code in there and reformat it and talk through what’s happening in this particular example. So right inside of that content wrapper component, we’re using those is food and is music Boolean variables to conditionally render, though, corresponding resources components. So if food is true and this post is in the category of food, we’ll render that. And if it’s music, we’ll do the same thing. And there, you can see we’re passing in all of the different props that are needed to render that.

And if we come back out and refresh one of our single.js template pages, we can see that our food resources are rendering just the way that we would expect, and that link would work appropriately if we come back into WP Admin or our home page. And if we find one and the food category or music, we can open up Kinfolk Synth DIY and see what our music resources component looks like, and all that looks great. And if we find one that actually is in both categories, we can see that it actually, at the bottom, renders both of those components just the way that we would expect it to.

OK. So now that we’ve got our site the way that we want it, let’s move on to talking about how to deploy this site. Now, I have a section of the GitHub repository created for us, and actually, I’ve created an entirely separate deployed branch in the finish branch. So running Git check out finished, we’ll get you that. And you can also just deploy that branch directly to Atlas, which is WP Engine’s headless WordPress hosting solution.

This gives you both a WordPress installation and a Node.js container, and you can sign up for a free sandbox account by just clicking this button on the Atlas landing page. Take you to a really quick forum, and as you can see, the price is zero. You may still have to put in a credit card that we use just for fraud prevention purposes, but you can have a free account to play around with any of this to test it, out to learn headless to your heart’s desire. So I’ll go ahead and open up the WP Engine dashboard to get started deploying this site to Atlas.

The first thing I’m actually going to do is open up my list of sites, and I’m going to open up my production WordPress site. So actually, this ACF headless site that you see right here, where I’m going to open up the WP Admin in a new window, is the parent site for the zip file that you all are using locally. So I made a zip of that using WP export, and that’s actually what I’m going to use for my production deployment.

From there, I’ll click into the Atlas tab and then click Create App. And I’m presented with this option. I’m going to choose Pull From Repo, and then click Continue. And if I haven’t authenticated with GitHub, this is where you would, but since I already am, I can just go ahead and select my repository. So we’ll go ahead and select the repository we’re using for this project, click Continue, then I’m going to deploy my app in US Central.

From here, it allows me to choose a branch, and like I said, I’m going to use Finished. There’s also options if you’re using a monorepo, which we’re not, and I’m going to leave the I Have My WordPress Installation checked, and Search For My ACF Headless Site. Now, here, I’m going to want to, actually, instead of using a .env file, I’m going to want to copy over two environment variables from my files project.

So the first one is next public WordPress URL. Those are the same environment variables that we set in our local project, and I’ll copy the link to my production WordPress install in there. Then I’ll add another environment variable, and this one is going to be for our Faust secret key. So I’ll go ahead and copy that from the Faust Settings menu, and pop that in there and set the key for that to Faust Secret Key.

And once that’s done, I can go ahead and click Create App, and Atlas will begin the process of building and deploying my app. During the Atlas build process, it will run NPM install, and your NPM build command for whatever framework that you’re using. And then once all of that code has been built, it will deploy that Node container for you to our network. And so once all of that spins for just a second, we should get a success message, and then we can click the URL that’s provided for us and actually look at our site live.

So we’ll get our success message, and then we can go ahead and open up this URL in another tab. And there we can see that our site looks exactly like it did locally, and it’s pulling in all of the correct data, all of the correct images, and even is sucking in all of our ACF content. A number of our posts look really great, and our very performant on Atlas– I’m actually using some really large images here, so if you see some slowness, it’s certainly due to my choices.

The Atlas platform is chocked full of features that modern JavaScript developers will appreciate. Sadly, there’s not enough time in this presentation to run through them all in detail.

But Atlas does a really good job of bringing together important details from both the frontend and backend parts of your headless ecosystem into one convenient dashboard, where you can look at individual build logs and the build output, examine deployments, environment variables that you used for a particular build, as well as have access to additional settings or features that you can enable, such as preview environments, where you can create an additional environment for every PR that’s created against your GitHub repo, or create environment webhooks to rebuild particular parts of your app or CDN as you make changes to WordPress.

All of these things become possible with the Atlas platform, and it really lowers the barrier for getting started building with headless WordPress.

Wow. Congratulations like I said that was a lot to cover in 25 minutes. Please feel free to keep on practicing after the presentation, and hit me up if you have any questions getting started with the demo resources. If you’re interested in learning more about headless but need a space to do it, sign up for a free Atlas Sandbox account. In addition to deploying your own code repositories, like we’ve done today, you can also get started with some of our premade blueprints, which are one click Project stacks that can help you see what a headless project looks like when it’s finished.

We have a basic scaffold example, a portfolio example with some custom post types, and a new ecommerce example that integrates with big commerce through some of their APIs. Our team and developer relations is really passionate about meeting you wherever you are on your journey towards learning headless. So check out our headless developer’s roadmap for a step-by-step series of articles and videos that build on one another to help backfill some of the concepts we’ve just glossed over today.

The headless ecosystem is young, but it’s growing quickly. If you need help on a project, want to talk to some people using headless WordPress in production, or just want to hang out where the magic happens, join the roughly 1,000 other developers in our Discord focused on headless, where we chat, help each other with projects, and help get you moving. Thank you so much for coming to this talk. I look forward to collaborating with you on all of your future headless projects.

Get started.

Build faster, protect your brand, and grow your business with a WordPress platform built to power remarkable online experiences.