❗ This is not a tutorial ❗
In https://zalgorithm.com/link-to-the-previous-and-next-posts/ I added “previous” and “next” navigation links to the bottom of each post. It worked, but I haven’t pushed the change to the live site. My concern is that the loader function on blog.$slug.tsx
is now making three requests – first it pulls in a post based on the URL’s slug
param, then it makes two requests to get data for the previous and next posts. This seems inefficient.
A possible workaround is to add a plugin to the WordPress site that extends the WPGraphQL plugin to return previousPost
and nextPost
data.
I made a start on that yesterday, but stopped when I realized I didn’t understand what I was doing. I’ll have another look at it.
Extending the WPGraphQL plugin
I’ll start by going through this tutorial: https://www.wpgraphql.com/docs/build-your-first-wpgraphql-extension.
… that got me somewhere:
// wpgraphql-previous-next.php
<?php
/**
* Plugin Name: WPGraphQL Previous Next
* Description: Adds previousPost and nextPost fields to the data returned by the WPGraphQL plugin
* Version: 0.1
* Author: scossar
*/
add_action( 'graphql_register_types', 'add_previous_and_next_post_to_wpgraphql_schema' );
function add_previous_and_next_post_to_wpgraphql_schema() {
register_graphql_field( 'Post', 'previousPost', [
'type' => 'String',
'description' => 'Once implemented, will return the previousPost from a Post query',
'resolve' => function() {
return 'the previousPost field is returning a string for now, later it will return a post';
}
]);
}
Going back to the example code I was looking at yesterday (https://github.com/kellenmace/pagination-station/blob/main/pagination-fields.php), this statement is making more sense:
use WPGraphQL\Model\Post;
WPGraphQL\Model\Post
is the type that the previousPost
and nextPost
fields are being added to. The code is here: https://github.com/wp-graphql/wp-graphql/blob/develop/src/Model/Post.php.
These are the WPGraphQL\Model\Post
fields:
// wp-graphql/src/Model/Post.php
...
/**
* Class Post - Models data for the Post object type
*
* @property int $ID
* @property string $post_author
* @property string $id
* @property string $post_type
* @property string $authorId
* @property string $authorDatabaseId
* @property int $databaseId
* @property string $date
* @property string $dateGmt
* @property string $contentRendered
* @property string $contentRaw
* @property string $titleRendered
* @property string $titleRaw
* @property string $excerptRendered
* @property string $excerptRaw
* @property string $post_status
* @property string $status
* @property string $commentStatus
* @property string $pingStatus
* @property string $slug
* @property array $template
* @property bool $isFrontPage
* @property bool $isPrivacyPage
* @property bool $isPostsPage
* @property bool $isPreview
* @property bool $isRevision
* @property bool $isSticky
* @property string $toPing
* @property string $pinged
* @property string $modified
* @property string $modifiedGmt
* @property string $parentId
* @property int $parentDatabaseId
* @property int $editLastId
* @property array $editLock
* @property string $enclosure
* @property string $guid
* @property int $menuOrder
* @property string $link
* @property string $uri
* @property int $commentCount
* @property string $featuredImageId
* @property int $featuredImageDatabaseId
* @property string $pageTemplate
* @property int $previewRevisionDatabaseId
*
* @property string $captionRaw
* @property string $captionRendered
* @property string $altText
* @property string $descriptionRaw
* @property string $descriptionRendered
* @property string $mediaType
* @property string $sourceUrl
* @property string $mimeType
* @property array $mediaDetails
*
* @package WPGraphQL\Model
*/
...
This statement in the example code was also confusing:
use WP_Post;
It was causing my IDE to generate the warning The use statement with non-compound name 'WP_Post' has no effect
. I’m guessing that’s because my file wasn’t namespaced.
I’ll go through the example code from top to bottom:
'resolve' => function( Post $resolving_post )
The argument that’s passed to the anonymous 'resolve'
function is Post $resolving_post
. Post
is indicating the type of the argument. It’s not a WordPress post, it’s a WPGraphQL\Model\Post
.
if ( is_post_type_hierarchical( $resolving_post->post_type ) )
This condition is checking to see if the WPGraphQL\Model\Post
that’s being passed to the function is a hierarchical
post, for example, a “page” or a hierarchical custom post type.
If it is a hierarchical (try typing that 10 times) post, it’s passed as an argument to:
private function get_previous_page_id( Post $page ): int {
return get_adjacent_page_id( $page, -1 );
}
Note that the type of argument is again set to WPGraphQL\Model\Post
. That function calls get_adjacent_page_id
with the argument set to the type WP_Post
???:
private function get_adjacent_page_id( WP_Post $page, int $direction ): int {
$args = [
'post_type' => $page->post_type,
'order' => 'ASC',
'orderby' => 'menu_order',
'post_parent' => $page->post_parent,
'fields' => 'ids',
'posts_per_page' => -1
];
$pages = get_posts( $args );
$current_key = array_search( $page->ID, $pages );
$does_adjacent_page_exist = isset( $pages[ $current_key + $direction ] );
if ( $does_adjacent_page_exist ) {
return $pages[ $current_key + $direction ];
}
return 0;
}
}
get_adjacent_page_id
is back in familiar territory. It makes a call to the WordPress get_posts
function with the $args
array. The 'post_parent'
argument will limit the results to just the siblings of the post that’s been passed to the function. The 'fields'
argument is telling get_posts
to just return 'ids'
. The 'posts_per_page'
argument of -1
is used to tell the query to return all of the siblings of the post.
Non-hierarchical posts are handle by the part of the 'resolve'
function that’s after the initial condition:
$post = get_post( $resolving_post->postId );
$GLOBALS['post'] = $post;
setup_postdata( $post );
$next_post = get_next_post();
wp_reset_postdata();
return $next_post ? new Post( $next_post ) : null;
Going through it line by line:
$post = get_post( $resolving_post->postId );
That’s just a call to the WordPress get_post
function.
$GLOBALS['post'] = $post;
Sets $post
as the current global post. This is needed for the next two lines:
setup_postdata( $post );
$next_post = get_next_post();
setup_postdata
seems to be needed so that get_next_post
can be called on the post that’s been passed to the 'resolve'
function. The documentation isn’t clear on that – I thought it was only needed for setting the post for template tags, so I’m not sure.
get_next_post
is a WordPress function that returns the next post that’s adjacent to the current post.
wp_reset_postdata
is a WordPress function that’s used to restore the global $post
object to its original state.
I feel confident enough to create a plugin based on the code that’s here: https://github.com/kellenmace/pagination-station/blob/main/pagination-fields.php.
… hmm… I’m still stumped by something in that code:
$previous_post_id = get_previous_page_id( $resolving_post );
return $previous_post_id ? new Post( $previous_post_id ) : null;
The problem is that the constructor for WPGraphQL\Model\Post
is expecting an argument of the type WP_Post
, not an int
.
I think the code should be:
$previous_post_id = get_previous_page_id( $resolving_post );
$previous_post = get_post( $previous_post_id);
return $previous_post ? new WPGraphQL\Model\Post( $previous_post ) : null;
…but I don’t need the previous or next fields for hierarchical posts. I can simplify the code by just returning null
from that condition:
<?php
/**
* Plugin Name: WPGraphQL Adjacent Posts
* Description: Extends the WPGraphQL plugin to add fields for adjacent posts.
* Version: 0.1
* Author: scossar
* Plugin URI: https://github.com/scossar/wp-graphql-adjacent-posts
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'graphql_register_types', 'add_adjacent_post_fields_to_wpgraphql_schema' );
/**
* Registers adjacent post fields.
*
* @return void
*/
function add_adjacent_post_fields_to_wpgraphql_schema() {
register_graphql_fields( 'Post', [
'previousPost' => [
'type' => 'Post',
'description' => __( 'Previous post', 'zalgorithm' ),
'resolve' => function ( WPGraphQL\Model\Post $resolving_post ) {
return resolve_adjacent_post( $resolving_post, 'previous' );
}
],
'nextPost' => [
'type' => 'Post',
'description' => __( 'Next post', 'zalgorithm' ),
'resolve' => function ( WPGraphQL\Model\Post $resolving_post ) {
return resolve_adjacent_post( $resolving_post, 'next' );
}
],
'previousPostInCategory' => [
'type' => 'Post',
'description' => __( 'Previous post in same category', 'zalgorithm' ),
'resolve' => function ( WPGraphQL\Model\Post $resolving_post ) {
return resolve_adjacent_post( $resolving_post, 'previous', true );
}
],
'nextPostInCategory' => [
'type' => 'Post',
'description' => __( 'Next post in same category', 'zalgorithm' ),
'resolve' => function ( WPGraphQL\Model\Post $resolving_post ) {
return resolve_adjacent_post( $resolving_post, 'next', true );
}
]
] );
}
/**
* Returns the next or previous post. Returns null if no post is found.
*
* @param \WPGraphQL\Model\Post $resolving_post The post to find the adjacent post for.
* @param string $direction Whether to return the 'next' or 'previous' post.
* @param boolean $in_same_term Whether to limit the next post to the post's category.
*
* @return \WPGraphQL\Model\Post|null
*/
function resolve_adjacent_post( WPGraphQL\Model\Post $resolving_post, $direction, $in_same_term = false ) {
// Currently not setup to handle hierarchical posts.
if ( is_post_type_hierarchical( $resolving_post->post_type ) ) {
return null;
}
$current_post = get_post( $resolving_post->postId );
$GLOBALS['post'] = $current_post;
setup_postdata( $current_post );
// Any string other than 'next' will result in querying for the previous post.
$adjacent_post = $direction === 'next' ? get_next_post( $in_same_term ) : get_previous_post( $in_same_term );
wp_reset_postdata();
return $adjacent_post ? new WPGraphQL\Model\Post( $adjacent_post ) : null;
}
Plugin repo: https://github.com/scossar/wp-graphql-adjacent-posts.
This is great! It will be simple to use these fields to add navigation to the bottom of each post. I’ll put that off until tomorrow.