Crash Course: Build a Headless WordPress App with Faust.js, WPGraphQL & Advanced Custom Fields

Grace Erixon Avatar

·

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:

Completed page showing all albums
Completed page showing album details page
Completed page showing individual song details page

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.

Local 'Create a site' screen

Choose a name for your new site, and then click Continue.

Local 'What's your site's name?' screen

Select the Preferred configuration for your environment. Then, click Continue.

Local 'Choose your environment' screen

Lastly, specify values for the WordPress username and WordPress password. Be sure to remember these values! Once that is complete, click Add Site.

Local 'Set up WordPress' screen

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.

Local screen showing WordPress site running

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 for Faust in the WordPress plugin repository. Install and activate this plugin.
  • From the Plugins > Add new menu, search for WPGraphQL in the WordPress plugin repository. Install and activate this plugin, adding a GraphQL 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.

Faust's settings page showing Front-end site URL and Secret Key

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 for Advanced Custom Fields in the WordPress plugin repository. Install and activate this plugin, adding a Custom Fields tab to your WP Admin sidebar. The ACF 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, and Load Terms.
  • Select Term ID as the Return Value.
  • Select Checkbox for the Appearance.

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 and Return 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 for Filters 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).

WordPress screen showing how to add a new Song

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).

WordPress screen showing how to add a new Album

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:

Faust.js app directory opened in VS Code

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 [albumSlug].js file and use that inside of our GraphQL client.

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> &#x2190; 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"> &#x2190; 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"> &#x2190; 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> &#x2190; View All Albums</p>
      </Link>
      <h1>{songData.songTitle}</h1>
      <p>Song Length: {songData.length}</p>
      <p>
        Genre:&nbsp;
        {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"> &#x2190; 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"> &#x2190; View All Albums</p>
      </Link>
      <h1 className="title">{songData.songTitle}</h1>
      <p className="details">Song Length: {songData.length}</p>
      <p className="details">
        Genre:&nbsp;
        {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.