Back and Forward Links for Hierarchical Pages
In hierarchical structures, navigation plays a key role. In this note, we will figure out how to "activate" the "Previous" and "Next" buttons so that the user can easily move between documentation pages of any nesting level.
Task:
There is a hierarchical custom post type wiki
, where parent pages (0 nesting level) are archive pages, and internal pages are documents. In the template, there are "Previous" and "Next" buttons that need to be "activated" - turned into links to the previous and next posts (pages). Thanks to them, the user should be able to navigate from the parent page to the end, considering any level of nesting (hierarchical structure).
Read also:
- Fetching Multiple Adjacent Posts with One Query.
- For flat posts (post type post), use the function get_adjacent_post().
/** * Retrieves objects of the previous and next documentation pages relative to the current one. * * @return Wiki_Item[]|null[] */ public function get_adjacent_pages() { global $chapter_ids; $walker = new class extends \Walker_Page { public function start_el( &$output, $data_object, $depth = 0, $args = [], $current_object_id = 0 ) { global $chapter_ids; $chapter_ids[] = (int) $data_object->ID; } }; // Add the index page ID to the beginning of the list, which won't be included in wp_list_pages() $index_page_id = $this->get_index_page()->get_id(); $chapter_ids = [ $index_page_id ]; wp_list_pages( [ 'post_type' => $this->get_post_type(), 'child_of' => $index_page_id, 'echo' => false, 'walker' => $walker, ] ); $data = [ 'prev' => null, 'next' => null, ]; foreach ( $chapter_ids as $index => $chapter_id ) { if ( $chapter_id === $this->get_id() ) { $data['prev'] = isset( $chapter_ids[ $index - 1 ] ) ? new self( $chapter_ids[ $index - 1 ] ) : null; $data['next'] = isset( $chapter_ids[ $index + 1 ] ) ? new self( $chapter_ids[ $index + 1 ] ) : null; break; } } unset( $chapter_ids ); return $data; }
Usage in the template:
<div class="documents__nav-buttons"> <div class="documents-nav"> <?php $wiki_item = \Wiki_Item( get_the_ID() ); $wiki_items = $wiki_item->get_adjacent_pages(); $prev_url = $wiki_items['prev'] ? $wiki_items['prev']->get_url() : null; $next_url = $wiki_items['next'] ? $wiki_items['next']->get_url() : null; ?> <a class="documents-nav__item documents-nav__item--prev <?= $prev_url ? '' : 'documents-nav__item--disabled' ?>" href="<?= $prev_url ?>"> <svg class="documents-nav__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12" fill="none"> <path d="M6.88462 1L1.5 6M1.5 6L6.88462 11M1.5 6H15.5"/> </svg> </a> <a class="documents-nav__item documents-nav__item--next <?= $next_url ? '' : 'documents-nav__item--disabled' ?>" href="<?= $next_url ?>"> <svg class="documents-nav__icon" xmlns="http://www.w3.org/2000/svg" width="16" height="12" viewBox="0 0 16 12" fill="none"> <path d="M9.11538 1L14.5 6M14.5 6L9.11538 11M14.5 6H0.499999"/> </svg> </a> </div> </div>
Looks like this:

The same code, but slightly unified
class Adjacent_Pages { /** Data of pages in pagination order */ private array $items_data; public function __construct( string $post_type, int $child_of = 0 ) { $this->set_items_data( $post_type, $child_of ); } /** * Retrieves objects of the previous and next page relative to the current one. * Considers the hierarchical nesting. * * @return array{0:WP_Post|null, 1:WP_Post|null} Previous and next post. */ public function get_prev_next( ?WP_Post $current_post = null ): array { if( ! $current_post ){ $current_post = $GLOBALS['post']; } foreach ( $this->items_data as $index => $item ) { if ( $item->ID !== $current_post->ID ) { continue; } $prev_item = $this->items_data[ $index - 1 ] ?? null; $next_item = $this->items_data[ $index + 1 ] ?? null; break; } return [ isset( $prev_item ) ? get_post( $prev_item->ID ) : null, isset( $next_item ) ? get_post( $next_item->ID ) : null, ]; } private function set_items_data( string $post_type, int $child_of ): void { $walker = new class extends \Walker_Page { public array $items_data = []; public function start_el( &$output, $data_object, $depth = 0, $args = [], $current_object_id = 0 ) { $this->items_data[] = (object) [ 'ID' => (int) $data_object->ID, 'post_parent' => (int) $data_object->post_parent, 'post_title' => $data_object->post_title, ]; } }; wp_list_pages( [ 'post_type' => $post_type, 'child_of' => $child_of, 'echo' => false, 'sort_order' => 'ASC', 'sort_column' => 'menu_order, post_title', 'walker' => $walker, ] ); $this->items_data = $walker->items_data; unset( $walker ); // just in case } }
Usage:
$chapter_post_id = 654; [ $prev, $next ] = ( new Adjacent_Pages( 'wiki', $chapter_post_id ) )->get_prev_next(); $prev_url = $prev ? get_permalink( $prev ) : ''; $next_url = $next ? get_permalink( $next ) : '';
Similar code but without using wp_list_pages()
To achieve this, replace the method set_items_data() in the above class Adjacent_Pages:
class Native_Adjacent_Pages extends Adjacent_Pages { protected function set_items_data( string $post_type, int $child_of ): void { $pages = get_pages( [ 'post_type' => $post_type, 'child_of' => $child_of, 'hierarchical' => false, 'sort_order' => 'ASC', 'sort_column' => 'menu_order, post_title', ] ); $this->items_data = ( new Kama_Make_Pages_Tree( $pages ) )->get_tree_flat_data(); } }
Usage is the same:
$chapter_post_id = 654; [ $prev, $next ] = ( new Native_Adjacent_Pages( 'wiki', $chapter_post_id ) )->get_prev_next(); $prev_url = $prev ? get_permalink( $prev ) : ''; $next_url = $next ? get_permalink( $next ) : '';
We will also need a class that works with the tree:
/** * Gets a flat array of hierarchical posts {@see get_pages()}, then * creates a nested tree from them with `children` elements, then * creates flat array in the order of that tree elements. */ final class Kama_Make_Pages_Tree { public array $parent_groups; public array $top_parents; private array $wp_post_fields = [ 'ID' => 'int', 'post_parent' => 'int', 'post_title' => 'string', ]; /** * @param \WP_Post[] $pages An array of pages objects obtained with {@see get_pages()}. */ public function __construct( $pages ){ $pages = $this->prepare_pages( $pages ); $this->parent_groups = self::group_by_parent_id( $pages ); $this->top_parents = $this->parent_groups[0]; // zero level posts } public function get_tree_flat_data(): array { return $this->collect_flat_data( $this->get_tree() ); } public function get_tree(): array { $tree = $this->top_parents; $this->fill_tree_recursively( $tree ); return $tree; } private static function group_by_parent_id( array $pages ){ $groups = []; foreach( $pages as $p ){ $parent_id = (int) $p->post_parent; $groups[ $parent_id ][] = $p; } return $groups; } private function collect_flat_data( $pages_tree ): array { $flat_data = []; foreach( $pages_tree as $obj ){ $flat_data[] = $obj; if( ! empty( $obj->children ) ){ foreach( $this->collect_flat_data( $obj->children ) as $_obj ){ $flat_data[] = $_obj; } } } return $flat_data; } /** * Fill each passed item's `children` field based on existing $parent_groups. * * @param object[] $parents */ private function fill_tree_recursively( & $parents ): void { foreach( $parents as & $parent ){ if( empty( $this->parent_groups[ $parent->ID ] ) ){ continue; } $parent->children = $this->parent_groups[ $parent->ID ]; $this->fill_tree_recursively( $this->parent_groups[ $parent->ID ] ); } unset( $parent ); // just in case } /** * @param \WP_Post[] $pages */ private function prepare_pages( $pages ): array { $unset_fields = array_diff_key( (array) reset( $pages ), $this->wp_post_fields ); $unset_fields = array_keys( $unset_fields ); foreach( $pages as & $p ){ $p = (object) (array) $p; // cast to stdClass foreach( $unset_fields as $name ){ unset( $p->$name ); } foreach( $this->wp_post_fields as $name => $type ){ settype( $p->$name, $type ); } } return $pages; } }
Conclusion
Now you know how to create navigation between nested pages, documents, and posts in WordPress using the "next page" and "previous page" buttons. This navigation can be implemented through wp_list_pages(), get_pages(), classes for traversing the page tree, and even without third-party plugins. Whether you are working with CPT, Wiki pages, hierarchical post types, child or parent pages — this method is universal for any nested structures in WordPress.