In my recent “What is Atlas Content Modeler?” blog post, I described Atlas Content Modeler (ACM) as a single tool that allows you to create content models and expose them in the WPGraphQL/REST API schema– something that previously required cobbling together several other plugins.
In this post, we’ll walk through all the features that ACM brings to the table, including the new Taxonomy and Relationships features. We’ll see how we can easily build our content models in the WordPress admin, then see an example Next.js app that consumes the data those models provide and uses it to render the pages of our app.
What We’ll Build
To showcase ACM’s superpowers, we’re going to build the most exciting app imaginable:
🎉 A Healthcare Provider Registry! 🎉 🙃

We’ll use ACM to create the content models we need in WordPress, then we’ll query for that data from a decoupled frontend Next.js app.
First things first. Let’s set the scene. 🌆
We’ve been tasked with building a web app that provides a listing of healthcare professionals. It needs to display a number of details about each provider including the locations where they practice.
In our talks with the client, we’ve learned that the following three types of pages are required for the minimum viable product:
Pages
/providers
– This “Providers” page should contain a list of cards, one for each provider./provider/123
– This “Provider” page should contain the info about an individual provider. “123” in this example represents the post ID of the provider./location/456
– This “Location” page should contain the info about an individual location. “456” in this example represents the post ID of the location.
Content Models
Now that we know what pages need to exist in the frontend of our app, we need to think about what content models we need to create to store the necessary data. After some deliberation, we come up with these:
Provider
- Name – Text field. The person’s name.
- Credentials – Text Field (MD or DO, for example)
- Profile Photo – Media Field.
- Bio – Rich Text field. This is a blurb about the provider.
- Practicing Since – Number field. This is the year the provider began practicing medicine.
- Locations – Relationship field. These relationships between
Provider
→Location
s represent the locations where the provider practices. - Specialty – Taxonomy. These taxonomy terms represent the provider’s specialties (Emergency Medicine, Pediatrics, etc.).
Location
- Name – The name of the location.
- Description – A description of the location.
- Photo – A photo of the location.
Now that we have our game plan together for what pages we’ll need and what content models we’ll need to set up in Atlas Content Modeler, let’s hop to it!
Create Content Models
If you’d like to follow along, you can spin up a new local WordPress site, then install and activate the WPGraphQL and Atlas Content Modeler plugins.
Let’s create our Location
model first, since it’s a bit simpler.
Location Model
In the WordPress admin sidebar, we’ll go to Content Modeler
> Models
and click the button to create a new model.
We’ll fill out the fields as follows:
- Give the model a
Singular Name
of “Location” and aPlural Name
of “Locations”. - Stick with the auto-generated
Model ID
of “location”. - Set the
API Visibility
to “Public” so we’ll be able to query forLocation
data from our decoupled JS app. - Select a
Model Icon
. - Enter a
Description
for the model.

Then click the button to create the new model.
In the WordPress admin sidebar, you’ll see that a menu item has been added for our new Location
custom post type.
Location Model Fields
Hit the +
button to add a new field to our Location
model.
Fist, let’s create the Name
field to store the name of each location. We’ll set the field up like this:
- Select
Text
for the type of field. - Enter a
Name
of “Name”. - Stick with the auto-generated
API Identifier
of “name”. - Select an
Input Type
of “Single line”.

Then click the button to create the field.
We’ll create the Description and Photo fields in a similar fashion. They’ll end up looking like this:
Description:
- Type of field:
Rich Text
. Name
: “Description”.API Identifier
: “description”.

Photo:
- Type of field:
Media
. Name
: “Photo”.API Identifier
: “photo”.

That’s it in terms of fields for our Location
model.
Next let’s see how our content creators can create and edit Location
data. From the WordPress admin sidebar, go to Locations
> Add New
. You’ll see the fields we registered reflected here.

Go ahead and create a few dummy Location
posts so we’ll have some data to work with.
Location Model Query
At this point, our Location
model and its fields have been created, and ACM has automatically added them to the GraphQL schema for us. This means that things are already set up for us to query for Location data from our decoupled frontend JS app! Next let’s fire off a test query to try that out.
Head back to the Content Modeler
> Models
page.
Click the ...
icon next to our Location model, then click Open in GraphiQL
.

This will shoot you over to the embedded GraphiQL IDE that the WPGraphQL plugin provides and auto-populate it with a query and fragment for the Location
model’s data.
Go ahead and click the ▶️ icon to fire off the query, and you’ll see that the data for the Location
posts you had created comes back in the response. Magic! ✨

We’ll make use of some of these GraphQL fields in our Next.js app in just a bit. For now, let’s turn our attention to the other Provider
model we need to create.
Provider Model
In the WordPress admin sidebar, head back to Content Modeler
> Models
and click the button to create a new model.
We’ll fill out the fields as follows:
- Give the model a
Singular Name
of “Provider” and aPlural Name
of “Providers”. - Stick with the auto-generated
Model ID
of “provider”. - Set the
API Visibility
to “Public” so we’ll be able to query for Provider data from our decoupled JS app. - Select a
Model Icon
. - Enter a
Description
for the model.

Then we’ll click the button to create the new model.
In the WordPress admin sidebar, you’ll see that a menu item has been added for our new Provider
custom post type.
Provider Model Fields
Hit the +
button to add a new field.
The first several fields we’ll add the same way we added fields to the Location
model. Here are the settings to use:
- Name – Make this a single-line
Text
field with aName
of “Name” and anAPI Identifier
of “name”. - Credentials – Make this a single-line
Text
field with aName
of “Credentials” and anAPI Identifier
of “credentials”. - Bio – Make this a
Rich Text
field with aName
of “Bio” and anAPI Identifier
of “bio”. - Practicing Since – Make this a
Number
field with aName
of “Practicing Since” and anAPI Identifier
of “practicingSince”. ForNumber Type
, select “Integer”.
Next, we’ll see our first and only Relationship
field. This kind of field is very useful for maintaining relationships between posts. In our app, we’re going to use it to keep track of which Provider
s practice at which Location
s.
Click the +
button to add one more field. Configure it like this:
- Select a field type of
Relationship
. - Enter a
Name
of “Locations”. - Stick with the auto-generated
API Identifier
of “locations”. - For
Model to Reference
, select “Locations”. This tells ACM which model to use at the other end of this relationship. - For
Connections
, select “Many to Many”, since many of our healthcareProvider
s will practice at many differentLocation
s. - Enter a
Description
of “Locations where the provider practices” to help us remember what this relationship represents.

Click the button to create this field.
Specialty Taxonomy
We’re done creating our fields for the Provider
model, but we still need a way to group together Provider
s based on their specialty (Emergency Medicine, Pediatrics, etc.). That’s a perfect use-case for a WordPress taxonomy.
From the WordPress admin sidebar, go to Content Modeler
> Taxonomies
.
In the Add New
section, configure a new taxonomy thusly:
- Enter a
Singular Name
of “Specialty”. - Enter a
Plural Name
of “Specialties”. - Stick with the auto-generated
Taxonomy ID
of “specialty”. - For
Models
, select “Providers”. - Set
API Visibility
to “Public” to make sure this taxonomy is added to the GraphQL schema.

Click the button to create the taxonomy.
As a final step in this section, head to Providers
> Add New
in the WordPress admin sidebar. Then create a few dummy Provider
posts so we have some data to work with.
Provider Model Query
Our Provider
model has been created, including its locations
Relationship
field and the Specialty
taxonomy. Just as we did for the Location
model, let’s fire off a test query to take it for a spin.
Head back to the Content Modeler
> Models
page.
Click the ...
icon next to our Provider
model, then click “Open in GraphiQL”.
This will once again shoot you over to the embedded GraphiQL IDE that the WPGraphQL plugin provides. This time, it will be auto-populated with a query and fragment for the Provider
model’s data.
If it’s not there already, add in fields to get the Specialty
taxonomy term names for each Provider, like this:
specialties {
nodes {
name
}
}
Go ahead and click the ▶️ icon to fire off the query, and you’ll see that the data for the Provider
posts you had created comes back in the response.

Done! Our Location
and Provider
content models are now fully built out, and we’ve seen how we can query for each via WPGraphQL. Now let’s query for this data from our frontend Next.js app.
Next.js App Walkthrough
You can follow these steps to get up and running with the Next.js app on your machine:
- Clone down the Next.js app repo.
- Create a
.env.local
file inside of the app’s root folder. Open that file in a text editor and paste inNEXT_PUBLIC_WORDPRESS_API_URL=http://acm-demo.local/graphql
, replacinghttp://acm-demo.local
with the domain for your local WordPress site. This is the endpoint that Apollo Client will use when it sends requests to your WordPress backend. - Run
npm install
(oryarn
) to install the app’s NPM dependencies. - Run
npm run dev
to get the server running locally. - You should now be able to visit http://localhost:3000/ in a web browser and see the app’s homepage.
Providers Page
Let’s first take a look at the Providers page. Navigate to /providers
. You should see a list of Provider
cards. Let’s open up pages/providers.js
in a code editor to see how it works.
export default function Providers({ providers }) {
return (
<Layout>
<h1>Providers</h1>
<ProvidersList providers={providers} />
</Layout>
);
}
export async function getStaticProps() {
const response = await client.query({
query: GET_PROVIDERS,
});
return {
props: {
providers: response?.data?.providers?.nodes ?? [],
},
};
}
Code language: JavaScript (javascript)
You can see that inside of Next.js’ getStaticProps()
function, we’re using Apollo Client to query for the list of providers. That data is then passed into the Providers
component as a prop.
The GraphQL query that we’re firing off to get this data looks like this:
const GET_PROVIDERS = gql`
query getProviders {
providers(first: 10) {
nodes {
...ProviderCardFields
}
}
}
${PROVIDER_CARD_FIELDS}
`;
Code language: JavaScript (javascript)
…and the ProviderCardFields
fragment that it uses is being pulled in from components/ProviderCard.js
. It looks like this:
export const PROVIDER_CARD_FIELDS = gql`
fragment ProviderCardFields on Provider {
databaseId
uri
name
credentials
profilePhoto {
sourceUrl
altText
}
specialties {
nodes {
name
}
}
locations {
nodes {
databaseId
name
uri
}
}
}
`;
Code language: JavaScript (javascript)
You can see that these fields look very similar to the ones we used in the test query we fired off in the GraphiQL IDE.
These fields are broken out into a fragment for code colocation; from the components/ProviderCard.js
file, you can easily manage both the GraphQL fields being requested and how they’re used inside of the component in that same file.
Back in our pages/providers.js
file, you can see that we’re taking the list of providers
and rendering a ProvidersList
component.
The ProvidersList
component lives in components/ProvidersList.js
and looks like this:
export default function ProvidersList({ providers }) {
return (
<ul className="doctors-list">
{providers.map((provider) => (
<li key={provider.databaseId}>
<ProviderCard provider={provider} />
</li>
))}
</ul>
);
}
Code language: JavaScript (javascript)
You can see that it renders a ul
, then maps over the providers
and renders an li
for each with a ProviderCard
component inside.
The ProviderCard
component lives in components/ProviderCard.js
and looks like this:
export default function ProviderCard({ provider }) {
const { uri, name, credentials, profilePhoto, specialties, locations } =
provider;
return (
<article className="card">
<Link href={uri}>
<a>
<h2>
{name}, {credentials}
</h2>
</a>
</Link>
{profilePhoto ? (
<Link href={uri}>
<a>
<img src={profilePhoto.sourceUrl} alt={profilePhoto.altText} />
</a>
</Link>
) : null}
<h3>Specialty</h3>
{specialties.nodes.map((specialty) => specialty.name).join(", ")}
<h3>Locations</h3>
<ul className="locations-list">
{locations.nodes.map((location) => {
return (
<li key={location.databaseId}>
🏥 <Link href={location.uri}>{location.name}</Link>
</li>
);
})}
</ul>
</article>
);
}
Code language: JavaScript (javascript)
This component is responsible for rendering the individual cards that you see on the /providers
page that list the details about each provider.
There are a few noteworthy things here:
- The person’s name and profile photo (if they have one) are both links that point to that single
Provider
page. - We’re mapping over the
Specialty
taxonomy terms that this provider has and displaying them on a list. In your applications, you can even go a step further and create taxonomy archive pages using the WPGraphQL Tax Query extension. - We’re making use of our
Provider
=>Location
s relationships. We map over thelocations
and for each, render a link that points to that singleLocation
page.
Next, let’s explore the single Provider
page (#1 on the list above).
Click on a provider’s name or photo to be sent to their single Provider
page. It’ll look something like this:

Open up pages/provider/[id].js
in a code editor to see the code for this page.
Inside of getStaticProps()
, we’re firing off the GET_PROVIDER
query and passing through to it the provider’s id
that’s in the URL. When that provider’s data comes back, it is passed through to the Provider
component as a prop.
const GET_PROVIDER = gql`
query getProvider($id: ID!) {
provider(id: $id, idType: DATABASE_ID) {
name
credentials
bio
practicingSince
profilePhoto {
sourceUrl
altText
}
specialties {
nodes {
name
}
}
locations {
nodes {
databaseId
name
uri
}
}
}
}
`;
export async function getStaticProps(context) {
const { id } = context.params;
const response = await client.query({
query: GET_PROVIDER,
variables: { id },
});
const provider = response?.data?.provider;
if (!provider) {
return { notFound: true };
}
return {
props: { provider },
revalidate: 600,
};
}
Code language: JavaScript (javascript)
The GET_PROVIDER
query should give you a good sense of what it looks like to get the data for one of your ACM model’s individual posts.
The Provider
component that’s responsible for rendering the page looks like this:
export default function Provider({ provider }) {
const {
name,
credentials,
bio,
practicingSince,
profilePhoto,
specialties,
locations,
} = provider;
return (
<Layout>
<Link href="/providers">
<a className="providers-link">← View Providers</a>
</Link>
<article className="provider">
{profilePhoto ? (
<img src={profilePhoto.sourceUrl} alt={profilePhoto.altText} />
) : null}
<h1>
{name}, {credentials}
</h1>
<p className="practicing-since">Practicing since {practicingSince}</p>
<h2>Bio</h2>
<div dangerouslySetInnerHTML={{ __html: bio }} />
<h2>Specialty</h2>
{specialties.nodes.map((specialty) => specialty.name).join(", ")}
<h2>Locations</h2>
<ul className="locations-list">
{locations.nodes.map((location) => {
return (
<li key={location.databaseId}>
🏥 <Link href={location.uri}>{location.name}</Link>
</li>
);
})}
</ul>
</article>
</Layout>
);
}
Code language: JavaScript (javascript)
Again, we see that we’re looping over the locations
and rendering a link for each. Go ahead and click through to one of those single Location
pages.
The page you land on will look something like this:

Now let’s open up pages/location/[id].js
in a code editor to see how it works. You’ll notice a similar pattern to what we saw with the single Provider
page.
Inside of the getStaticProps()
function, we fire off a GET_LOCATION
query and pass to it the location’s id
that’s in the URL. When that Provider
‘s data comes back, it is passed through to the Location
component as a prop.
const GET_LOCATION = gql`
query getLocation($id: ID!) {
location(id: $id, idType: DATABASE_ID) {
name
description
photo {
sourceUrl
altText
}
}
}
`;
export async function getStaticProps(context) {
const { id } = context.params;
const response = await client.query({
query: GET_LOCATION,
variables: { id },
});
const location = response?.data?.location;
if (!location) {
return { notFound: true };
}
return {
props: { location },
revalidate: 600,
};
}
Code language: JavaScript (javascript)
Studying GET_LOCATION
gives you yet another opportunity to see how we can query for one of your ACM model’s individual posts.
Side-Note on Bi-Directional Relationship Queries
On this page, it would also be cool to be able to query for all Provider
s who practice at this Location
and provide links to each. That would involve querying the Provider
=> Location
relationship in the opposite direction– starting with the Location
and finding Provider
s related to it. At the time of this writing, ACM has not added the ability to do bi-directional relationship field queries like that, but the team is planning to add it in the near future.
The Location
component that’s responsible for rendering the page’s content looks like this:
export default function Location({ location }) {
const { photo, name, description } = location;
return (
<Layout>
<Link href="/providers">
<a className="providers-link">← View Providers</a>
</Link>
<article className="location">
{photo ? <img src={photo.sourceUrl} alt={photo.altText} /> : null}
<h1>{name}</h1>
<div dangerouslySetInnerHTML={{ __html: description }} />
</article>
</Layout>
);
}
Code language: JavaScript (javascript)
Here, we render the Location
‘s photo
if it has one, the name
of the location, and the description
.
That concludes the Next.js app walkthrough.
Summary
At this point, we’ve learned how to:
- Create ACM models
- Create new posts for those models in the WordPress admin
- Run test queries to get ACM model data using the GraphiQL IDE
- Fire off GraphQL queries to fetch ACM model data from our decoupled Next.js app
- Use the fetched ACM model data to render our React components
I hope all of that is helpful to you as you dive into working with Atlas Content Modeler! The team working on it has put a lot of work into producing a single, unified tool for creating content models, with support for taxonomies and relationships being recent additions. They’re not done, however! A number of other field types will be added in the near future, along with other features. Stay tuned!
How Can I Help?
I’m glad you asked! We would love for you to try out ACM on your own projects. You can provide us with feedback on it in the following ways:
- Click ”Send Feedback” at the top of the Content Modeler page in your WordPress admin area to share your thoughts with us.
- File bugs or feature requests in GitHub.
Thanks for your interest in the project! 🙌