This tutorial will teach you how to create a headless WordPress app using Faust.js, WPGraphQL, and Advanced Custom Fields (ACF). It assumes a basic understanding of JavaScript, React fundamentals, GraphQL fundamentals, and WordPress.
By the end of this tutorial, you will be able to:
- Create custom post types and fields using Advanced Custom Fields
- Create new posts for those models in the WordPress Admin dashboard
- Run test queries to get ACF data using the GraphiQL IDE
- Use Faust.js and Apollo Client to fetch ACF data in our decoupled Next.js app and render our React components
What We’ll Build
To showcase how to use Faust.js and ACF, we will build a site containing Taylor Swift’s discography.
This will require the following three types of pages:
We’ll use ACF to create the content models we need in WordPress, and then we’ll query for that data from a decoupled frontend Next.js and Faust.js app.
/albums
– This main “Albums” page should contain a gallery of each of Taylor Swift’s albums./albums/red-taylors-version
– This “Album details” page should have the info about an individual album. “red-taylors-version” in this example represents the slug of the album./songs/all-too-well-10-minute-version-taylors-version
– This “Song details” page should contain the info about an individual song. “all-too-well-10-minute-version-taylors-version” in this example represents the slug of the song.
Prerequisites
To get started, you’ll need to have both Node.js and npm installed. If you aren’t sure whether or not you have this software installed, you can run the following commands in your terminal:
node -v
npm -v
The terminal output should either tell you which versions you have installed or that it cannot find the commands node
or npm
to run them. Using a tool like nvm (Node Version Manager) to install and manage versions of Node.js on your machine can be helpful when working on multiple projects.
Configure Your Headless WordPress Site
Create a New Faust Project
To create a Faust project, run the following:
npx create-next-app \
-e https://github.com/wpengine/faustjs/tree/main \
--example-path examples/next/faustwp-getting-started \
--use-npm
Code language: PHP (php)
Now, cd
into your new app and copy the sample environment template:
cp .env.local.sample .env.local
Code language: CSS (css)
Finally, run the dev server:
npm run dev
You can now visit http://localhost:3000 to see your new project.
Next, let’s set up a local WordPress site that can be used as the data source for our example application. Then, we’ll populate the site with the specific data for this tutorial.
Create a WordPress Site with Local
Local is WP Engine’s local development tool, and it makes it very easy to work with WordPress locally. You can install the app and create a new WordPress site in a few steps.
Now, we’re ready to create a WordPress site! Click the +
button at the bottom left corner of the window. Select Create a new site
, then click Continue
.
Choose a name for your new site, and then click Continue
.
Select the Preferred
configuration for your environment. Then, click Continue
.
Lastly, specify values for the WordPress username
and WordPress password
. Be sure to remember these values! Once that is complete, click Add Site
.
Depending on your permissions, Local may ask for permission to make modifications to your system. After your site has been successfully installed, you will see it in the Local dashboard.
To access the WP Admin panel of your new site, click WP Admin
in the site details pane and authenticate with the username and password you created in the previous step.
Connect Your WordPress Site
Our current Faust.js app loads WordPress content from the demo site at https://faustexample.wpengine.com. We want to point it to a different WordPress site.
First, install and activate the Faust and WPGraphQL plugins on your site.
- From the
Plugins > Add new
menu, search forFaust
in the WordPress plugin repository. Install and activate this plugin. - From the
Plugins > Add new
menu, search forWPGraphQL
in the WordPress plugin repository. Install and activate this plugin, adding aGraphQL
tab to your WP Admin sidebar.
Once the necessary plugins have been installed, open the .env.local
file you created earlier in a code editor. It should look something like this:
# Your WordPress site URL
NEXT_PUBLIC_WORDPRESS_URL=https://faustexample.wpengine.com
# Plugin secret found in WordPress Settings->Headless
FAUST_SECRET_KEY=YOUR_PLUGIN_SECRET
Code language: PHP (php)
We need to update this information in our WordPress instance. The necessary values can be found in the WordPress Admin sidebar under Settings
> Faust
.
In the .env.local
file, update the NEXT_PUBLIC_WORDPRESS_URL
value with your WordPress site URL (including http://
or https://
) and the FAUST_SECRET_KEY
value with the secret key found on Faust’s Settings page.
Additionally, on the WordPress Admin Settings page, set the Front-end site URL
to match the URL running our frontend Faust.js app locally.
Install Plugins and Check Settings
With a basic headless WordPress site up and running, there are a few things left to install and a few settings to check.
There are two additional plugins we need to add:
- From the
Plugins > Add new
menu, search forAdvanced Custom Fields
in the WordPress plugin repository. Install and activate this plugin, adding aCustom Fields
tab to your WP Admin sidebar. TheACF
plugin is used to register our custom post types and custom fields. WPGraphQL for Advanced Custom Fields
is not currently available on the WordPress.org repository, so you must download it from GitHub. To install the plugin from GitHub, download the latest release zip file, upload the Zip file to your WordPress install, and activate the plugin. This is the WPGraphQL extension that exposes ACF data via the WPGraphQL schema.
Open the GraphQL > Settings
menu and check the option labeled Enable GraphQL Debug Mode
and then check the option labeled Enable Public Introspection
. Click Save Changes
.
With Debug Mode enabled you will get more helpful error output as you develop your application. Note that using Debug Mode is not recommended for production sites.
Create Custom Post Types and Custom Fields
Plan Content
Let’s think about what custom post types and custom fields we need to create to store the necessary data.
Songs
- Song Title – Text field. The song’s name.
- Length – Text field. The song’s timestamp.
- Lyrics – Text area field. The song’s formatted lyrics.
- Genre – Taxonomy. These taxonomy terms represent the song’s genres (Pop, Country, Alternative, etc.).
Albums
- Album Title – Text field. The album’s name.
- Release Date – Date picker field. The album’s release date.
- Cover – Image field. An uploaded image of the album’s cover.
- Track List – Relationship field. These relationships between
Album
→Songs
represent the songs on each album.
Now that we have our game plan together for what post types and fields we’ll need to set up using Advanced Custom Fields, let’s get to it!
Create Songs
Songs Post Type
Let’s create our Songs
post type first since it’s a bit simpler. In the WordPress Admin sidebar, we’ll go to ACF
> Post Types
. This should bring us to this page where we can add a new post type.
Click + Add New
and fill out the following fields:
- Set
Plural Label
to “Songs” - Set
Singular Label
to “Song” - Set
Post Type Key
to “song”
The post type still needs to be shown in GraphQL so we can access the posts we create. Further down on this screen, click the slider for Advanced Configuration
. Select the furthest right tab titled GraphQL
and enable the Show in GraphQL
slider. The autogenerated GraphQL Single Name
and GraphQL Plural Name
can be kept without editing.
Then click the Save Changes
button to create the new post type.
In the WordPress Admin sidebar, you’ll see that a menu item has been added for our new Songs
custom post type.
Songs Custom Fields
After saving the new Songs
post type, you will get a success banner on the top of the screen alerting you – Songs post type created
. There are several options underneath this message. Click on the first option to Add fields to Songs
.
This will navigate you to the form to add a new field group and auto-fill the title of this field group as “Song fields”. Within this form, we can create all the fields that should belong to a song.
Our first field will be the song’s title. We’ll set the field up like this:
- Select
Text
for the Field Type. - Enter a
Field Label
of “Song Title.” - Stick with the auto-generated
Field Name
of “song_title.” - Leave the
Default Value
empty.
We’ll create the Length, Lyrics, and Genre fields in a similar way by clicking the + Add Field
button underneath each completed field.
The Length field will look like this:
- Select
Text
for the Field Type. - Enter a
Field Label
of “Length.” - Stick with the auto-generated
Field Name
of “length.” - Leave the
Default Value
empty.
The Lyrics field will look like this:
- Select
Text Area
for the Field Type. - Enter a
Field Label
of “Lyrics.” - Stick with the auto-generated
Field Name
of “lyrics.”
As the last field for the Song
post type, we need to add a Genre
field. Grouping Songs
based on their genre (Alternative, Country, Pop, etc.) is a perfect use case for a WordPress taxonomy. The Genre field will look like this:
- Select
Taxonomy
for the Field Type. - Enter a
Field Label
of “Genre.” - Stick with the auto-generated
Field Name
of “genre.” - Select
Tag
as the type of taxonomy to be displayed. - Check the slider for
Create Terms
,Save Terms
, andLoad Terms
. - Select
Term ID
as theReturn Value.
- Select
Checkbox
for theAppearance
.
Finally, scroll down past the Fields section to the Settings section. Ensure the Rules say, “Show this field group if Post Type is equal to Song.”
Then, just below the Settings section (and the ACF PRO ad), in the GraphQL section, check the slider for Show in GraphQL
to access this data in our frontend app.
Click Save Changes
at the top of the screen.
That’s it for our Songs
custom post type and custom fields! Now, let’s turn to the Albums
post type.
Albums Post Type
In the WordPress Admin sidebar, navigate again to ACF
> Post Types
to add a new post type.
Click + Add New
and fill out the following fields:
- Set
Plural Label
to “Albums.” - Set
Singular Label
to “Album.” - Set
Post Type Key
to “album.”
Just as we did with the Song
post type, we need to enable the Album
post type to be shown in GraphQL. Further down on this screen, click the slider for Advanced Configuration
. Select the furthest right tab titled GraphQL
and enable the Show in GraphQL
slider. The autogenerated GraphQL Single Name
and GraphQL Plural Name
can be kept without editing.
Then click the Save Changes
button to create the new post type.
In the WordPress Admin sidebar, you’ll see that a menu item has been added for our new Albums
custom post type.
Albums Model Fields
After saving the new Albums
post type, you will get a success banner on the top of the screen alerting you – Albums post type created
. Click on the link to Add fields to Albums
.
This will navigate you to the form to add a new field group and auto-fill the title of this field group as “Album fields”. Within this form, we can create all the fields that should belong to an album.
Our first field will be the album’s title. We’ll set the field up like this:
- Select
Text
for the Field Type. - Enter a
Field Label
of “Album Title.” - Stick with the auto-generated
Field Name
of “album_title.” - Leave the
Default Value
empty.
We’ll create the Release Date, Cover, and Track List fields in a similar way, but using different Field Types
.
The Release Date field will look like this:
- Select
Date Picker
for the Field Type. - Enter a
Field Label
of “Release Date.” - Stick with the auto-generated
Field Name
of “release_date.” - Select “m/d/Y” as the
Display Format
andReturn Format
.
The Cover field will look like this:
- Select
Image
for the Field Type. - Enter a
Field Label
of “Cover.” - Stick with the auto-generated
Field Name
of “cover.” - Select “Image ID” as the
Return Format
. - Select “All” for the
Library
.
The Track List field will look like this:
- Select
Relationship
for the Field Type. - Enter a
Field Label
of “Track List.” - Stick with the auto-generated
Field Name
of “track_list.” - Select “Song” for the
Filter by Post Type
. - Leave
Filter by Taxonomy
blank and leave all options forFilters
checked. - Select “Post Object” for the
Return Format
.
Finally, scroll down past the Fields section to the Settings section. Ensure the Rules say, “Show this field group if Post Type is equal to Album.”
Then, just below the Settings section (and the ACF PRO ad), in the GraphQL section, check the slider for Show in GraphQL
to access this data in our frontend app.
Click Save Changes
at the top of the screen.
That’s it for our Albums
custom post type and custom fields!
Populate Data about Taylor Swift’s Discography
Next, let’s look at how to create and edit Album
and Song
data.
Add Song Data
From the WordPress Admin sidebar, go to Songs
> Add New
. You’ll see the fields we registered reflected here. Create entries for each of Taylor Swift’s songs (Taylor’s Version, of course).
Add Album Data
From the WordPress Admin sidebar, go to Albums
> Add New
. You’ll see the fields we registered reflected here. Create entries for each of Taylor Swift’s albums (again, Taylor’s Version, of course).
Test a Query in the GraphiQL IDE
At this point, our custom post types and custom fields have been created and populated with data, and we have chosen to automatically add them to the GraphQL schema. This means that we are already set up to query for Songs
or Albums
data from our frontend JavaScript app! Let’s fire off a test query to try that out.
Head over to the embedded GraphiQL IDE that the WPGraphQL plugin provides and use the Query Composer to build a query using songs
or albums
. Here’s an example:
{
songs(first: 10) {
nodes {
songFields {
songTitle
length
lyrics
}
}
}
}
Go ahead and click the ▶️ icon to fire off the query, and you’ll see that the data for the Songs
posts you had created comes back in the response. Magic! ✨
Generating Possible types JSON
Finally, Apollo Client v3 requires you to provide a possibleTypes object
that maps interfaces to all their possible types. Faust provides a cli
command you can use on your package.json scripts to generate this file.
In the terminal, run the generate
script:
npm run generate
Done! Our Songs and Albums content models are now fully built out, and we’ve seen how we can query for them via WPGraphQL. Now let’s use this data in our frontend Faust.js app.
Build the Albums Page
Now that you have a WordPress site with the Taylor Swift discography data, we can get started with Faust.js.
In a code editor, open the Faust.js project directory that you created. After opening the project directory, your text editor should look like this:
Create albums.js
To get started, create a new file titled albums.js
inside the pages
directory. We want to create a new page displaying a grid of all of Taylor Swift’s albums.
Next.js uses a routing method known as page-based routing, meaning that generally, the routes of your site or application will correspond to the file structure of your /pages
folder. In this case, the albums.js
file corresponds to your site’s local URL /albums
.
To get the data we need, start by adding an import statement to the top of our pages/albums.js
file:
import { gql, useQuery } from "@apollo/client";
Code language: JavaScript (javascript)
Add another import statement for a component that we will build in a later step – AlbumCard
.
import AlbumCard from "../components/AlbumCard";
Code language: JavaScript (javascript)
Then, back inside the WordPress Admin dashboard, use the Query Composer to build a query that will return all the data about each album for our home page: album cover image, release date, database ID, and slug. The completed query should look like this:
{
albums {
nodes {
albumFields {
releaseDate
cover {
node {
databaseId
mediaItemUrl
}
}
}
databaseId
slug
}
}
}
Then, back in our code editor, we will create a GraphQL query named GET_ALBUMS
underneath the import
statement. Copy the query we built in the GraphiQL IDE and paste it into a gql
function. Remember to wrap query strings in the gql
function to parse them into query documents.
const GET_ALBUMS = gql`
query getAlbums {
albums {
nodes {
albumFields {
cover {
node {
databaseId
mediaItemUrl
}
}
releaseDate
}
databaseId
slug
}
}
}
`;
Code language: JavaScript (javascript)
Next, create a function to hold our Albums page. Inside the Albums
function, pass the GET_ALBUMS
query to the useQuery
hook.
The useQuery
React hook is the primary API for executing queries in an Apollo application. To run a query within a React component, call useQuery
and pass it a GraphQL query string – like the one we just created. When your component renders, useQuery
returns an object from Apollo Client that contains loading
, error
, and data
properties you can use to render your UI.
export default function Albums() {
const { loading, error, data } = useQuery(GET_ALBUMS);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return ();
}
Code language: JavaScript (javascript)
Inside the Albums
component, we need to write an expression that iterates over the albums
array to display each. We will do this by rendering an unordered list, then map over the albums
and render a list item containing an AlbumCard
component for each album.
return (
<ul>
{data.albums.nodes.map((album) => (
<li key={album.databaseId}>
<AlbumCard album={album} />
</li>
))}
</ul>
);
Code language: JavaScript (javascript)
Create the AlbumCard Component
Now, let’s create the component that we are calling in this main file. Inside of the components
directory, create a new file called AlbumCard.js
.
First, add an import statement for the Next.js Link component.
import Link from "next/link";
Code language: JavaScript (javascript)
Then, create a function called AlbumCard
that accepts an individual album’s data as a prop
. Inside of the return
statement, display an image of the album’s cover wrapped in a link that points to the individual album details page that we will build out later.
export default function AlbumCard({ album }) {
return (
<Link href={`/albums/${album.slug}`}>
<img src={album?.albumFields.cover?.node?.mediaItemUrl} />
</Link>
);
}
Code language: JavaScript (javascript)
Create the Layout Component
Now, let’s create the component that we can use to wrap around every other JSX element to follow Next.js’ rule that JSX expressions must have one parent element. Inside of the components
directory, create a new file called Layout.js
.
Add an import statement for the Next.js Head component. Add the following code to the return
statement of the Layout
function.
import Head from "next/head";
export default function Layout({ children }) {
return (
<>
<Head>
<title>TS Discography</title>
</Head>
<main>{children}</main>
</>
);
}
Code language: JavaScript (javascript)
Back in albums.js
, import this file:
import Layout from "../components/Layout";
Code language: JavaScript (javascript)
Then, wrap everything currently inside of the return
statement inside of a <Layout>
component.
<Layout>
...
</Layout>
Code language: HTML, XML (xml)
Style the Albums Page
Check out the site running in the browser! You can see the ten album covers displayed, but it’s not very pretty. Let’s add some styles to this page! In the styles
directory, add the following SCSS declarations to the existing _base.scss
file:
.gallery {
margin: 5% 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 15px;
}
.galleryItem {
list-style-type: none;
opacity: 1;
}
.galleryItem:hover img {
opacity: .6;
}
Code language: CSS (css)
Back in the albums.js
file, add the .gallery
class to the ul
element and the .galleryItem
class to the li
element.
<ul className="gallery">
...
<li className="galleryItem" key={album.databaseId}>
Code language: HTML, XML (xml)
Review the albums.js
File
After all of these steps, your albums.js
file should look like this:
import { gql, useQuery } from "@apollo/client";
import AlbumCard from "../components/AlbumCard";
import Layout from "../components/Layout";
const GET_ALBUMS = gql`
query getAlbums {
albums {
nodes {
albumFields {
cover {
node {
databaseId
mediaItemUrl
}
}
releaseDate
}
databaseId
slug
}
}
}
`;
export default function Albums() {
const { loading, error, data } = useQuery(GET_ALBUMS);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<Layout>
<ul className="gallery">
{data.albums.nodes.map((album) => (
<li className="galleryItem" key={album.databaseId}>
<AlbumCard album={album} />
</li>
))}
</ul>
</Layout>
);
}
Code language: JavaScript (javascript)
Build the Single Album Details Page
Create [albumSlug].js
Now, let’s create the page that displays an individual album’s details. Inside of the pages
directory, create a new folder called albums
with a file inside called [albumSlug].js
.
To get the data we need, start by adding an import statement to the top of our albums/[albumSlug].js
file:
import { gql, useQuery } from "@apollo/client";
Code language: JavaScript (javascript)
Then, back inside the WordPress Admin dashboard, use the Query Composer to build a query that will return all of the data about the specific album for our album details page: album title, cover image, release date, and the list of all of the songs on that album.
We need to determine which album data to retrieve based on the slug from the URL. Let’s hard-code the slug red-taylors-version
into the query to test that it returns the correct information.
{
album(id: "red-taylors-version", idType: SLUG) {
albumFields {
albumTitle
cover {
node {
mediaItemUrl
altText
}
}
releaseDate
trackList {
nodes {
... on Song {
id
slug
songFields {
songTitle
}
}
}
}
}
}
}
Code language: JavaScript (javascript)
However, this query needs to be able to return the data for any album, not just Red (Taylor’s Version). We can add a variable called albumSlug
into the query instead of hard-coding the value. Below the query, we can set a value for the query to ensure it still works as expected.
query getAlbumDetails($albumSlug: ID!) {
album(id: $albumSlug, idType: SLUG) {
albums {
albumTitle
cover {
sourceUrl
mediaItemUrl
altText
}
releaseDate
trackList {
... on Song {
id
songs {
songTitle
}
slug
}
}
}
}
}
Code language: PHP (php)
Then, back in our code editor, we will create a GraphQL query named GET_ALBUM_DETAILS
underneath the import
statement. Copy the query we built in the GraphiQL IDE and paste it into a gql
function. Remember to wrap query strings in the gql
function to parse them into query documents.
const GET_ALBUM_DETAILS = gql`
query GetAlbumDetails($albumSlug: ID!) {
album(id: $albumSlug, idType: SLUG) {
albumFields {
albumTitle
cover {
node {
mediaItemUrl
altText
}
}
releaseDate
trackList {
nodes {
... on Song {
id
slug
songFields {
songTitle
}
}
}
}
}
}
}
`;
Code language: PHP (php)
Import the Next.js Router component. The useRouter
hook’s query
object allows us to access the dynamic route parameter albumSlug
that we created with the
file and use that inside of our GraphQL client.[albumSlug].js
import { useRouter } from "next/router";
Code language: JavaScript (javascript)
Then, create a function called Album()
with a blank return statement for now. Inside this Album
function, pass the GET_ALBUM_DETAILS
query and the albumSlug
retrieved from the useRouter
hook to the useQuery
hook to retrieve the requested data on the current album.
export default function Album() {
const { query = {} } = useRouter();
const { albumSlug } = query;
const { loading, error, data } = useQuery(GET_ALBUM_DETAILS, {
variables: { albumSlug }
});
const albumData = data?.album?.albumFields;
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return ();
}
Code language: JavaScript (javascript)
Now that we have access to the individual album’s data, we’ll focus on the view for the page. Start with adding import statements for our Layout
component and the Next.js Link component.
import Layout from "../../components/Layout";
import Link from "next/link";
Code language: JavaScript (javascript)
Inside of the return statement of the Album
function, we will build the JSX elements. First, wrap everything inside of the Layout
component. At the top of the screen, we will also add a button for users to return to the main page of the site. List out the details of the album that we entered through ACF in WordPress: title, release date, cover image, and the list of songs.
return (
<Layout>
<Link href="/albums">
<p> ← View All Albums</p>
</Link>
<h1>{albumData.albumTitle}</h1>
<p>Released on {albumData.releaseDate}</p>
<img src={albumData?.cover.node.mediaItemUrl} alt={albumData.cover.node.altText} />
<h3>Track List</h3>
<ol>
{albumData.trackList.nodes.map((song) => (
<li key={song.id}>
<Link href={`/songs/${song.slug}`}>
<a>{song.songFields.songTitle}</a>
</Link>
</li>
))}
</ol>
</Layout>
);
Code language: JavaScript (javascript)
Style the Single Album Page
Check out the site running in the browser and navigate to the album details page by clicking on one of the cover images. You can see all of the information that we want, but it’s not very pretty. Let’s add some styles to this page!
Go back to the _base.scss
file and add the following additional SCSS declarations:
.backButton {
text-align: right;
margin: 2% 5%;
font-weight: bold;
}
.backButton:hover {
text-decoration: underline;
}
.title {
text-align: center;
}
.details {
text-align: center;
}
.cover {
display: block;
margin: auto;
}
.trackList {
text-align: center;
text-decoration: none;
list-style-type: none;
padding-inline-start: 0;
}
.listItem a {
text-decoration: none;
color: black;
}
.listItem a:hover {
font-weight: bold;
}
.lyrics {
white-space: pre-wrap;
}
Code language: CSS (css)
Use these SCSS classes and assign them to their respective elements in the [albumSlug].js
file:
<p className="backButton"> ← View All Albums</p>
...
<h1 className="title">{albumData.albumTitle}</h1>
<p className="details">Released on {albumData.releaseDate}</p>
<img className="cover" src={albumData?.cover.node.mediaItemUrl} alt={albumData.cover.node.altText} />
<h3 className="details">Track List</h3>
<ol className="trackList">
...
<li className="listItem" key={song.id}>
Code language: HTML, XML (xml)
Review the [albumSlug].js
File
After all of these steps, your [albumSlug].js
file should look like this:
import { gql, useQuery } from "@apollo/client";
import { useRouter } from "next/router";
import Layout from "../../components/Layout";
import Link from "next/link";
const GET_ALBUM_DETAILS = gql`
query GetAlbumDetails($albumSlug: ID!) {
album(id: $albumSlug, idType: SLUG) {
albumFields {
albumTitle
cover {
node {
mediaItemUrl
altText
}
}
releaseDate
trackList {
nodes {
... on Song {
id
slug
songFields {
songTitle
}
}
}
}
}
}
}
`;
export default function Album() {
const { query = {} } = useRouter();
const { albumSlug } = query;
const { loading, error, data } = useQuery(GET_ALBUM_DETAILS, {
variables: { albumSlug }
});
const albumData = data?.album?.albumFields;
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<Layout>
<Link href="/albums">
<p className="backButton"> ← View All Albums</p>
</Link>
<h1 className="title">{albumData.albumTitle}</h1>
<p className="details">Released on {albumData.releaseDate}</p>
<img className="cover" src={albumData?.cover.node.mediaItemUrl} alt={albumData.cover.node.altText} />
<h3 className="details">Track List</h3>
<ol className="trackList">
{albumData.trackList.nodes.map((song) => (
<li className="listItem" key={song.id}>
<Link href={`/songs/${song.slug}`}>
<a>{song.songFields.songTitle}</a>
</Link>
</li>
))}
</ol>
</Layout>
);
Code language: JavaScript (javascript)
Build the Song Details Page
Creating the individual song details page will be very similar to creating the individual album details page.
Create [songSlug].js
First, let’s create a page to display an individual song’s details. Inside of the pages
directory, create a new folder called songs
with a new file inside of it called [songSlug].js
.
To get the data we need, we need to import several things:
import { gql, useQuery } from "@apollo/client";
import { useRouter } from "next/router";
Code language: JavaScript (javascript)
Then, back inside the WordPress Admin dashboard, use the Query Composer to build a query that will return all of the data about the specific song for our song details page: song title, length, lyrics, and genre.
We need to determine which song data to retrieve based on the slug from the URL, just as we did with the album details page. We can use a variable in the query again to accept the song slug and return data about the corresponding song.
query getSongDetails($songSlug: ID!) {
song(id: $songSlug, idType: SLUG) {
songFields {
songTitle
lyrics
length
genre {
nodes {
name
}
}
}
}
}
Code language: PHP (php)
Then, back in our code editor, we will create a GraphQL query named GET_SONG_DETAILS
underneath the import
statement. Copy the query we built in the GraphiQL IDE and paste it into a gql
function. Remember to wrap query strings in the gql
function to parse them into query documents.
const GET_SONG_DETAILS = gql`
query getSongDetails($songSlug: ID!) {
song(id: $songSlug, idType: SLUG) {
songFields {
songTitle
lyrics
length
genre {
nodes {
name
}
}
}
}
}
`;
Code language: PHP (php)
Then, create a function called Song()
with a blank return statement for now. Inside of this Song
function, pass the GET_SONG_DETAILS
query and the songSlug
retrieved from the useRouter
hook to the useQuery
hook to retrieve the requested data on the current song.
export default function Song() {
const { query = {} } = useRouter();
const { songSlug } = query;
const { loading, error, data } = useQuery(GET_SONG_DETAILS, {
variables: { songSlug }
});
const songData = data?.song?.songFields;
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return ();
}
Code language: JavaScript (javascript)
Now that we have access to the individual song’s data, we’ll focus on the view for the page. Start with adding import statements for our Layout
component and the Next.js Link component.
import Layout from "../../components/Layout";
import Link from "next/link";
Code language: JavaScript (javascript)
Inside the return statement of the Song
function, we will build the JSX elements. This will be very similar to what we built for the albumSlug
page. List out the details of the song that we entered through ACF in WordPress: title, length, genre, and lyrics.
return (
<Layout>
<Link href="/albums">
<p> ← View All Albums</p>
</Link>
<h1>{songData.songTitle}</h1>
<p>Song Length: {songData.length}</p>
<p>
Genre:
{songData
.genre.nodes.map((genre) => genre.name)
.join(", ")}
</p>
<h3>Lyrics</h3>
<div>{songData.lyrics}</div>
</Layout>
);
Code language: JavaScript (javascript)
Style the Song Details Page
Check out the site running in the browser and navigate to the song details page by clicking on one of the song titles from the album’s tracklist. You can see all of the information that we want, but it’s not very pretty. Let’s add some styles to this page!
We can use the existing classes that we added to _base.scss
file for the album details page. Assign these classes to their respective elements in the [songSlug].js
file:
<p className="backButton"> ← View All Albums</p>
...
<h1 className="title">{song.songTitle}</h1>
<p className="details">Song Length: {song.length}</p>
<p className="details">
...
<h3 className="details">Lyrics</h3>
<div className="details lyrics">{songData.lyrics}</div>
Code language: HTML, XML (xml)
Review the [songSlug].js File
After all of these steps, your [songSlug].js
file should look like this:
import { gql, useQuery } from "@apollo/client";
import { useRouter } from "next/router";
import Layout from "../../components/Layout";
import Link from "next/link";
const GET_SONG_DETAILS = gql`
query getSongDetails($songSlug: ID!) {
song(id: $songSlug, idType: SLUG) {
songFields {
songTitle
lyrics
length
genre {
nodes {
name
}
}
}
}
}
`;
export default function Song() {
const { query = {} } = useRouter();
const { songSlug } = query;
const { loading, error, data } = useQuery(GET_SONG_DETAILS, {
variables: { songSlug }
});
const songData = data?.song?.songFields;
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<Layout>
<Link href="/albums">
<p className="backButton"> ← View All Albums</p>
</Link>
<h1 className="title">{songData.songTitle}</h1>
<p className="details">Song Length: {songData.length}</p>
<p className="details">
Genre:
{songData
.genre.nodes.map((genre) => genre.name)
.join(", ")}
</p>
<h3 className="details">Lyrics</h3>
<div className="details lyrics">{songData.lyrics}</div>
</Layout>
);
}
Code language: JavaScript (javascript)
Done! 🎉
You should now be able to navigate back and forth between the home page, the album details page, and the song details page. Don’t forget to update your custom post type and field data as Taylor Swift releases her re-recordings. 😉
Congratulations on creating a headless WordPress site! Hopefully, you now have a good understanding of how you can leverage tools like Faust.js, ACF, and WPGraphQL to build headless WordPress sites.
If for any reason you weren’t able to follow along with the steps outlined in this post, you can access the finished tutorial at this GitHub repository.
Looking for a place to host your headless WordPress project? Check out WP Engine’s Atlas platform.