Due to wp_list_pages()
leveraging get_pages()
to actually retrieve the Page posts, and because get_pages()
implements it’s own query logic independent of WP_Query
‘s, it’s not possible to use arbitrary query variables which are not directly supported by get_pages()
, including year
, mothnum
, day
, date_query
, etc.
In order to continue using wp_list_pages()
to this end, I think the most elegant solution would be to implement a custom Walker_Page
class which you might pass to wp_list_pages()
as an argument in order to customize the generated HTML.
As an added benefit, this solution continues to use a single get_pages()
query instead of one query per year, and can fluidly handle grand-children pages and continue to scale as more years are added.
Tailored Walker_Page
In WordPress “Walkers” are classes which are designed to take lists representing hierarchical or tree-like data like that of a like of hierarchical post-types, taxonomies, or navigation menu items and turn them into markup for display.
wp_list_pages()
provides a parameter to pass a custom Walker to it in place of the Walker_Page
class it uses by default, allowing you to customize the markup the function produces.
// mytheme/lib/class-year-accordion-walker-page.php
// (or functions.php, or some other theme library code location)
/**
* A Walker_Page customized to additionally group first-level pages into lists based
* on their publication year.
**/
class Year_Accordion_Walker_Page extends Walker_Page {
/**
* Creates the opening markup for a year's accordion group.
*
* @param string &$output The string to append output to.
* @param int $year The year which to build group markup for.
* @param boolean $is_active If this year matches the publication date for the current page, as indicated by "child_of".
* @param array $args Any additional arguments passed to `walk()`.
**/
public function start_accordion_group( &$output, $year, $is_active = false, $args = [] ) {
// Setup HTML classes to be added to various parts of the markup.
$container_classes = [];
$header_classes = [ 'ui-accordion-header' ];
$content_classes = [ 'ui-accordion-content' ];
if( $is_active ) {
$container_classes[] = 'current_page_ancestor';
$header_classes[] = 'ui-accordion-header-active';
$content_classes[] = 'ui-accordion-content-active';
}
// Add this accordion group's opening markup to the output, joining each line with the configured indentation.
$output .= implode(
"\n\t",
[
'<li class="' . implode( ' ', $container_classes ) . '">',
'<h3 class="' . implode( ' ', $header_classes ) . '">' . $year . '</h3>',
'<ul class="' . implode( ' ', $content_classes ) . '">',
]
);
}
/**
* Creates the closing markup for a year's accordion group.
*
* @param string &$output The string to append output to.
* @param int $year The year which to build group markup for.
* @param boolean $is_active If this year matches the publication date for the current page, as indicated by "child_of".
* @param array $args Any additional arguments passed to `walk()`.
**/
public function end_accordion_group( &$output, $year, $is_active = false, $args = [] ) {
$output .= implode(
"\n",
[
"\t</ul>", // Close the accordion group's page list.
'</li>', // Close the accordion group's container li.
]
);
}
/**
* Display array of Pages, grouped by the topmost pages' publication year.
*
* $max_depth = 0 means display all levels.
* $max_depth > 0 specifies the number of display levels.
*
* @param array $pages An array of WP_Post items.
* @param int $max_depth The maximum hierarchical depth.
* @param mixed ...$args Optional additional arguments.
* @return string Markup representing $pages grouped by publication year.
**/
public function walk( $pages, $max_depth = 0, ...$args ) {
$output="";
// Invalid parameter or nothing to walk.
if ( $max_depth < 0 || empty( $pages ) )
return $output;
$options = $args[0]; // When used with `walk_page_tree()` this is an assoc array of any additional arguments.
$current_page_id = $args[1]; // When used with `walk_page_tree()` this is the current post ID.
$active_page_year = null; // Keep track of which year the current_page_id Page was published in for custom classes, etc.
$parent_field = $this->db_fields['parent']; // Inherited from `Walker_Page` - will always be `ID`.
$parent_id = isset( $options['child_of'] ) ? $options['child_of'] : 0; // The ID of the top-level common parent, if any.
$top_level_pages_by_year = []; // A mapping of year => top-level pages (parent is $parent_id, or no parent if $parent_id is not set).
$child_pages_by_parent = []; // A mapping of parent_id => pages, collecting all pages whose parent is not $parent_id (or who have a parent, if $parent_id is not set)
// Split $pages into $child_pages_by_parent and $top_level_pages_by_year.
foreach( $pages as $page ) {
$year = date( 'Y', strtotime( $page->post_date ) );
if( $page->ID === $current_page_id )
$active_page_year = $year; // If this is the current page, record the year in which it was published.
// If this is a grandchild page, we don't care about it's publish date - just add it to the map of parent_id => child pages.
if( ( empty( $parent_id ) && ! empty( $page->$parent_field ) ) || ( ! empty( $parent_id ) && $page->$parent_field !== $parent_id ) ) {
if( ! isset( $child_pages_by_parent[ $page->$parent_field ] ) )
$child_pages_by_parent[ $page->$parent_field ] = [];
$child_pages_by_parent[ $page->$parent_field ][] = $page;
continue;
}
if( ! isset( $top_level_pages_by_year[ $year ] ) )
$top_level_pages_by_year[ $year ] = [];
$top_level_pages_by_year[ $year ][] = $page; // Add the top level page to it's respective year list.
}
// Build the markup, one year at a time.
foreach( $top_level_pages_by_year as $year => $pages ) {
// Build the opening markup for this year's accordion group.
$this->start_accordion_group( $output, $year, $year == $active_page_year, $args );
// Add each top-level page in the group's child tree markup as per usual.
foreach( $pages as $page )
$this->display_element( $page, $child_pages_by_parent, $max_depth, 1, $args, $output );
// Close this year's accordion group.
$this->end_accordion_group( $output, $year, $year == $active_page_year, $args );
}
return $output;
}
}
With the above class available, wp_list_pages()
can be called to utilize it as such:
// Template file
wp_list_pages(
[
'title_li' => '&',
'child_of' => $id,
'show_date' => 'modified',
'date_format' => $date_format,
'walker' => new Year_Accordion_Walker_Page,
]
);
Finishing Touches
For the icing on the cake, you might even create a “template tag” function to load and leverage this class when necessary:
// functions.php
// (or other library file loaded by functions.php)
/**
* Display pages in an accordion, grouped by year. Loads the customized
* Walker_Page if it's not already available.
*
* @param array $args Array of wp_list_pages() arguments.
**/
function wpse390524_page_year_accordion( $args = [] ) {
if( ! class_exists( 'Year_Accordion_Walker_Page' ) )
require_once get_stylesheet_directory() . '/lib/class-year-accordion-walker-page.php';
$args['walker'] = new Year_Accordion_Walker_Page;
wp_list_pages( $args );
}
// Template file
wpse390524_page_year_accordion(
[
'title_li' => '&',
'child_of' => $id,
'show_date' => 'modified',
'date_format' => $date_format,
]
);