Atlas Content Modeler has the potential to become a BIG deal in the headless WordPress space. Up until this point, creating the content models for your headless WordPress site has required you to:
- Register your custom post types and taxonomies with Custom Post Type UI
- Expose custom fields in the GraphQl schema with Advanced Custom Fields + WPGraphQL for Advanced Custom Fields
- Create efficient many-to-many relationships between posts or users using MB Relationships + WPGraphQL MB Relationships
To complicate things further, many plugins designed before the headless WordPress era have aspects that make them less-than-ideal for use on headless sites. Some examples:
- Custom Post Type UI presents the user with several fields that don’t apply to headless architectures and unnecessarily clutter the interface.
- Advanced Custom Fields allows users to select from several jQuery-powered fields, even though virtually all modern JavaScript frameworks do not use jQuery (React/Vue/Svelte/etc.).
Some developers may prefer to swap out some of the plugins mentioned above for others or perhaps write custom code to register their custom post types and taxonomies. However, the central point remains: creating content models for headless WordPress sites is cumbersome and requires cobbling together several tools not designed with headless architectures in mind.
Introducing Atlas Content Modeler
Atlas Content Modeler (ACM) is an open-source project from WP Engine that strives to remove all of this complication and confusion and ultimately simplify the process of creating content models for your headless WordPress site. With a single tool, you’ll be able to create a new content model, define the custom fields and taxonomies for it, develop relationships between the model and other posts or users, and have all of that data automatically available to you via the WordPress REST API or WPGraphQL! 🤯
Not all of those features are in place as of the time of this writing, however. Currently, you can register content models and assign several custom fields to them. The Atlas Content Modeler team plans on adding support for other custom field types, taxonomies, and relationships are on the project roadmap.
Let’s dive in and get a feel for what working with ACM looks like in practice.
Content Modeler Real-World Usage
Let’s say that I’m a developer working on a headless WordPress site for a client of mine. The client runs a company that does construction and renovation work. In the discovery phase of the project, the client tells me that they want to manage the company’s projects in the WordPress admin and have the data for those projects displayed on the frontend of the site. Further discussion with the client reveals that we needed a “Project” content model that looks this:
Project
- Street Address (single-line text field)
- Contact Name (single-line text field)
- Work Order Number (single-line text field)
- Description of Work (rich text field)
- Cost (number field)
- Date of Service (date field)
- Property Photo (media field)
- Follow-up Inspection Required (boolean field)
Local Setup
Armed with that knowledge, I fire up a new local WordPress site and get to work. I install and activate the Atlas Content Modeler plugin. From the WordPress admin sidebar, I click on Content Modeler
, then click the Add New
button to create a new model.
Project Model Creation
I define the Single Name
, Plural Name
, Model ID
, select the Model Icon
and provide the Description
. I’m also careful to set the API Visibility to Public
since I want the data for this Project model to be added to the public GraphQL schema. That way, I’ll be able to query for Project data from my decoupled frontend app.
The page now looks like this:

I click the button to create the model. At this point, the Project
custom post type has been registered, and I see that a new Projects
menu item has been added to the WordPress Admin sidebar.
Next, we can define the custom fields that our model requires.
Custom Fields
I click on the +
icon to add a new field. This field will be for them Street Address
, so I’ll choose Text
for the field type. I’ll give it the name “Street Address” and go with the auto-generated “streetAddress” API identifier. We’ll make use of this API identifier later when we run queries to get our data. The field’s settings now look like this:

I click the button to create the field, then create the Contact Name
and Work Order Number
fields, both of which are also single-line text fields.
For the Description of Work
field, I choose Rich Text
the field type and give it a name and API identifier.

This process of adding fields is repeated for the Cost
, Date of Service
, Property Photo
and Follow-up Inspection Required
fields, registering them as Number
, Date
, Media
and Boolean
field types, respectively.
With all fields added, I can see all of my registered fields on the list.

Working with the Model in the WordPress Admin
Now that I’ve defined my content model, I want to see what the experience will be like for my client when they enter project data. I go to Projects
> Add New
in the WordPress, Admin sidebar to take a look. All the custom fields I defined are present, and they’re all displaying as the correct field types.
Next, I can fill them in with project dummy data and save the Project post.

Query for Data via WPGraphQL
Here’s where things get cool! 😎
From the WordPress Admin sidebar, I head back over to Content Modeler
. I click on the ellipsis (…
) icon, then click on Open in GraphiQL
.

Before my very eyes, WPGraphQL’s GraphiQL IDE page opens up with a query and fragment already pre-populated with all the data for my model. I click the ▶
icon to execute the query and see that the first Project I had created is included in the response, along with all of the data for its custom fields! 💥

This is powerful! Now, I’m ready to spin up a new frontend app using my favorite JavaScript framework (Next.js/Gatsby/Nuxt/SvelteKit/other). I can pull data from my headless WordPress backend, then fire off queries very similar to the one shown above.
Next.js App Example
Here’s an example of a Projects list page component in a Next.js app:
// pages/projects.js
import { gql } from "@apollo/client";
import Link from "next/link";
import { client } from "../lib/apolloClient";
import Layout from "../components/Layout";
const GET_PROJECTS = gql`
query getProjects {
projects(first: 10, after: null) {
nodes {
databaseId
streetAddress
propertyPhoto {
mediaItemUrl
altText
}
}
}
}
`;
export default function Projects(props) {
const { projects } = props;
return (
<Layout>
<h1>Projects</h1>
<ul className="projects-list">
{projects.map((project) => (
<li key={project.databaseId}>
<article className="project-card">
{project.propertyPhoto ? (
<Link href={`/project/${project.databaseId}`}>
<a>
<img
src={project.propertyPhoto.mediaItemUrl}
alt={project.propertyPhoto.altText}
className="property-photo"
/>
</a>
</Link>
) : null}
<h2>
<Link href={`/project/${project.databaseId}`}>
<a>{project.streetAddress}</a>
</Link>
</h2>
</article>
</li>
))}
</ul>
</Layout>
);
}
export async function getStaticProps() {
const response = await client.query({
query: GET_PROJECTS,
});
return {
props: {
projects: response.data.projects.nodes,
},
};
}
Code language: JavaScript (javascript)
As you can see in the example, I fire off a query to get Projects data, then map over the results displaying them in a list.
This Projects list page looks like this on the frontend of the site:

Here’s an example of a single Project page component in the same app:
// pages/project/[id].js
import { gql } from "@apollo/client";
import parse from "html-react-parser";
import Link from "next/link";
import { client } from "../../lib/apolloClient";
import Layout from "../../components/Layout";
const GET_PROJECT = gql`
query getProject($databaseId: ID!) {
project(id: $databaseId, idType: DATABASE_ID) {
streetAddress
contactName
workOrderNumber
descriptionOfWork
cost
dateOfService
propertyPhoto {
mediaItemUrl
altText
}
}
}
`;
function formatCurrency(number) {
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
return formatter.format(number);
}
const formatDate = (date) => new Date(date).toLocaleDateString();
export default function Project({ project }) {
const {
streetAddress,
contactName,
workOrderNumber,
descriptionOfWork,
cost,
dateOfService,
propertyPhoto,
} = project;
return (
<Layout>
<Link href="/projects">
<a>← Back to projects</a>
</Link>
<article className="single-project">
<h1>{streetAddress}</h1>
{propertyPhoto ? (
<img
src={propertyPhoto.mediaItemUrl}
alt={propertyPhoto.altText}
className="property-photo"
/>
) : null}
<ul className="project-details">
<li>
<span className="label">👤 Contact</span>
<span className="value">{contactName}</span>
</li>
<li>
<span className="label">📁 Work Order #</span>
<span className="value">{workOrderNumber}</span>
</li>
<li>
<span className="label">💰 Cost</span>
<span className="value">{formatCurrency(cost)}</span>
</li>
<li>
<span className="label">📅 Date of Service</span>
<span className="value">{formatDate(dateOfService)}</span>
</li>
</ul>
<div className="project-description">{parse(descriptionOfWork)}</div>
</article>
</Layout>
);
}
export function getStaticPaths() {
return {
paths: [],
fallback: "blocking",
};
}
export async function getStaticProps(context) {
const response = await client.query({
query: GET_PROJECT,
variables: {
databaseId: context.params.id,
},
});
const project = response?.data?.project;
if (!project) {
return { notFound: true };
}
return {
props: { project },
revalidate: 60,
};
}
Code language: JavaScript (javascript)
You can see that the GraphQL query being run here is for a single Project, which I’m identifying by its databaseId
(a.k.a. it’s “post ID” in WordPress parlance). Using the data in the response, the address, photo, details, and description for the project are rendered.
This single Project page looks like this on the frontend of the site:

The source code for this Next.js app is here, if you’d like to reference it: https://github.com/kellenmace/atlas-content-modeler-demo.
Where to Go from Here?
By this point, I hope you can see the potential the Atlas Content Modeler has!
The goal is for developers to no longer have to cobble together multiple tools that weren’t designed with headless WordPress in mind and then worry about exposing their data in the REST API / GraphQL schema. Instead, they can use ACM to register their custom content types and automatically expose them via REST/GraphQL all in one shot.
As mentioned at the beginning of this post, only some custom field types are currently supported, but the team plans to add other field types, taxonomies, relationship fields, and more soon. Stay tuned!
How Can I Help?
I’m glad you asked! We would love for you to be a beta tester and 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! 🙌