Custom Post Types
Using Custom Post Types with WPGraphQL
In order to use Custom Post Types with WPGraphQL, you must configure the Post Type to show_in_graphql using the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
show_in_graphql | boolean | Yes | If true, show the post type in the GraphQL Schema |
graphql_single_name | string | Yes | Camel case string with no punctuation or spaces. Needs to start with a letter (not a number) |
graphql_plural_name | string | No | Camel case string with no punctuation or spaces. Needs to start with a letter (not a number) |
graphql_description | string | No | Custom description for the type in the GraphQL schema. If not set, will fallback to post type’s description, then a default description |
graphql_kind | string | No | Allows the type representing the post type to be added to the graph as an object type, interface type or union type. Possible values are ‘object’, ‘interface’ or ‘union’. Default is ‘object’ |
graphql_resolve_type | callable | No | The callback used to resolve the type. Only used if “graphql_kind” is set to “union” or “interface” |
graphql_interfaces | array<string> | No | List of Interface names the type should implement. These will be applied in addition to default interfaces such as “Node” |
graphql_exclude_interfaces | array<string> | No | List of Interface names the type should not implement. This is applied after default and custom interfaces are added |
graphql_fields | array<$config> | No | Array of fields to add to the Type. Applied if “graphql_kind” is “interface” or “object” |
graphql_exclude_fields | array<string> | No | Array of fields names to exclude from the type. Applies if “graphql_kind” is “interface” or “object” |
graphql_connections | array<$config> | No | Array of connection configs to register to the type. Only applies if the “graphql_kind” is “object” or “interface” |
graphql_exclude_connections | array<string> | No | Array of connection names to exclude from the type |
graphql_union_types | array<string> | No | Array of possible types the union can resolve to. Only used if “graphql_kind” is set to “union” |
graphql_register_root_field | boolean | No | Whether to register a field to the RootQuery to query a single node of this type. Default true |
graphql_register_root_connection | boolean | No | Whether to register a connection to the RootQuery to query multiple nodes of this type. Default true |
graphql_exclude_mutations | array<string> | No | Array of mutations to prevent from being registered. Possible values are “create”, “update”, “delete” |
Registering a new Custom Post Type
This is an example of registering a new “docs” post_type and enabling GraphQL Support.
add_action( 'init', function() {
register_post_type( 'docs', [
'show_ui' => true, # whether you want the post_type to show in the WP Admin UI. Doesn't affect WPGraphQL Schema.
'labels' => [
//@see https://developer.wordpress.org/themes/functionality/internationalization/
'menu_name' => __( 'Docs', 'your-textdomain' ), # The label for the WP Admin. Doesn't affect the WPGraphQL Schema.
],
'hierarchical' => true, # set to false if you don't want parent/child relationships for the entries
'show_in_graphql' => true, # Set to false if you want to exclude this type from the GraphQL Schema
'graphql_single_name' => 'document',
'graphql_plural_name' => 'documents', # If set to the same name as graphql_single_name, the field name will default to `all${graphql_single_name}`, i.e. `allDocument`.
'public' => true, # shorthand that fills in the defaults for publicly_queryable, show_ui and others. Not an access control setting, see "Public vs Private Data" below
'publicly_queryable' => true, # this is the one WPGraphQL reads: set to false and entries are returned only to users who can edit them
] );
} );
Filtering an Existing Post Type
If you want to expose a Post Type that you don’t control the registration for, such as a post type registered in a third-party plugin, you can filter the Post Type registration like so, applying any of the arguments listed above:
add_filter( 'register_post_type_args', function( $args, $post_type ) {
// Change this to the post type you are adding support for
if ( 'docs' === $post_type ) {
$args['show_in_graphql'] = true;
$args['graphql_single_name'] = 'document';
$args['graphql_plural_name'] = 'documents'; # Don't set, and it will default to `all${graphql_single_name}`, i.e. `allDocument`.
}
return $args;
}, 10, 2 );
Using Custom Post Type UI
Custom Post Type UI is a popular WordPress plugin that enables users to register Custom Post Types and Custom Taxonomies from the WordPress dashboard via a user interface. The WPGraphQL for Custom Post Type UI plugin provides fields to Custom Post Type UI that allow you to set whether the Post Type or Taxonomy should show in GraphQL, and set the GraphQL Single Name and GraphQL Plural Name.
Public vs Private Data
WordPress treats being visible on the web and being available through an API as two separate decisions.
publicly_queryabledecides whether entries are reachable on the front end of your site, at a URL.show_in_restdecides whether they are available through the REST API.show_in_graphqldecides whether they are available through the GraphQL API.
WPGraphQL is deliberately stricter than WordPress here. We require the front end door to be open before we serve entries publicly through GraphQL:
Published entries are returned to an anonymous request when the post type is registered public => true or publicly_queryable => true. If both are false, entries are returned only to users with the capability to edit them.
public | publicly_queryable | Anonymous GraphQL request |
|---|---|---|
true | true | entries are returned |
true | false | entries are returned (this is how core registers page) |
false | true | entries are returned |
false | not set | nothing is returned, because it inherits false from public |
WordPress itself does not work this way. Its REST API gates on show_in_rest alone and returns any published entry, so a post type can be hidden from the front end and still readable through REST by anyone.
We take the stricter line on purpose. Plugins register post types all the time without considering whether their entries should be readable by anonymous API callers, and a GraphQL endpoint is often the most public surface a site has. Defaulting to closed means an extension has to opt in before its data is served to everyone, rather than opting out after the fact.
If your post type has no front end
This is the normal case for headless sites, where WordPress serves no pages at all. Set publicly_queryable explicitly:
register_post_type( 'docs', [
'public' => false, # no URLs, no admin UI
'publicly_queryable' => true, # but do serve entries through the API
'show_in_graphql' => true,
'graphql_single_name' => 'document',
'graphql_plural_name' => 'documents',
] );
Without that second line, publicly_queryable inherits whatever public is, so public => false silently closes the API to anonymous callers too, and queries come back empty with no error to explain why.
public is not an access control setting
It is a shorthand. WordPress uses it to fill in the defaults for publicly_queryable, show_ui, show_in_nav_menus, exclude_from_search and embeddable, and then never consults it again. Each of those can be set on its own, and once set, public has no further say.
So public => false means “this is not a normal, user facing content type.” It does not mean the entries are secret. WordPress registers several of its own post types this way, including the one that stores your navigation menus, whose content appears on every page of the site.
When WPGraphQL decides whether to serve entries to an anonymous caller it checks both flags, so setting either one is enough. The case that catches people out is leaving publicly_queryable unset, because then it quietly takes its value from public.
Querying the content type itself
Separate from its entries, the schema exposes a post type’s registration data as a ContentType:
{
contentType(id: "docs", idType: NAME) {
name
description
publiclyQueryable
}
}
Two rules decide what comes back.
A content type is exactly as visible as its entries. If an anonymous caller cannot read entries of the type, contentType returns null for them too. Describing a type whose content is withheld would only advertise that it exists. This follows the same stricter line as the table above, and it is one place we differ from the REST API, which serves the type object to anyone.
Configuration fields require the capability to edit the type’s entries. Without it, public, publiclyQueryable, showUi, labels and similar fields return null, while a smaller set always resolves: name, description, hierarchical, label, taxonomies, uri, graphqlSingleName, graphqlPluralName and showInGraphql. The isRestricted field tells you which case you are in. The REST API draws the same line, reserving capabilities, labels, supports, viewable and visibility for requests made with context=edit.
So a null from public does not mean the post type was registered public => false. It usually means the caller is not allowed to see that setting. Read isRestricted before reading anything into it.
Querying Custom Post Types
Querying content in a Custom Post type is similar to querying Posts & Pages. The difference being that the graphql_single_name and graphql_plural_name will be shown in the Schema.
So, assuming you registered the post type as shown above, with the graphql_plural_name of docs, you would be able to query like so:
{
docs {
nodes {
id
title
}
}
}
And if your graphql_single_name were Doc, you would be able to query a single Doc like so:
{
doc(id: "validIdGoesHere") {
id
title
}
}
Post Type Supports & Interfaces
All post types have the ContentNode Interface applied to their GraphQL Type.
WPGraphQL exposes fields that a post type has registered support for using the post_type_supports, and leaves out fields that a post type does not support.
Supported fields are applied to the GraphQL Type using Interfaces.
An example, would be the title field.
If your Custom Post Type supports the title field, the GraphQL Type representing your post type will have the NodeWithTitle Interface applied to it.
Mutating Custom Post Types
Mutating Custom Post types is pretty similar to mutating Posts & Pages.