Custom archive page based on array of categories and tags

This turned out to be a quite extensive little project for me.

BASIC IDEA

My approach was to go with a custom archive page as this seems to be the best approach here. The thing with custom taxonomies is that if you have a structure in place, changing that would become a real messy expedition. Also, I always try to stay away from custom queries if something can be done by the main query.

The big hurdle with a custom archive page (which I went with) is that WordPress does not offer this setup by default. I had to write a lot of custom code (and had to get some help on the rewrite part, special thanks to @Milo here) to achieve the end goal

WORKFLOW

(I have commented the code well, so I’m not going to go into depth here, I’m just going to touch on important points)

This is a very basic workflow of the whole process

  • Get the tags and categories from the current single post. For a more streamlined faster operation, only get the ID’s from the tags and categories

  • Add the tag and category ID’s to two separate strings, this is important as these two strings will be later used in a tax_query to display the relevant posts

  • Build a link to the virtual archive page. I went with a URL that will look like this: http://mysite.com/custom-archive/.

  • Set two new query_vars, cq and tq which will respectively hold a string of category ID’s and tag ID’s. This query_vars plus the respective value will be added to the link URL. This values will later be used to build our query

  • Append the custom query_vars to the URL of the link to our custom archive page.

  • Rewrite the URL so that WordPress can interpret it. (This section I received help from @Milo. Please see his answer to my question here)

  • Set the parse_query conditionals to false (Not really necessary to do all of them, but I did, it is just a case of better be safe than sorry). The most important one here and the one that you have to set is is_home() as the custom rewrite rules rewrite the URL to home. This causes is_home() to return true on this custom page, and we do not want this

  • Not necessary, but really handy, set a new conditional tag through parse_query and also create a conditional function, is_custom_archive() to save on unnecessary code

  • Not necessary, but set a custom template to use when any of the two query_vars is present in the URL. I have used custom-archive.php. You can simply make a copy of index.php and rename it to custom-archive.php. If you want to omit this, WordPress will simply use index.php to display the posts

  • Use pre_get_posts to set the tax_query to the main query to get posts according to the values of the two custom query_vars. This will alter the main query accordingly

  • For best interest, set a 404 if http://mysite.com/custom-archive/ is directly visited, this will have the same effect as visiting http://mysite.com/category/. If you omit this part, and you visit http://mysite.com/custom-archive/ directly, you will get all posts as you would on the home page as this page will then work the same as the home page

FEW IMPORTANT NOTES

  • You need to flush your permalinks after the process has been completed, otherwise it will not work

  • Change the template name, link name and query_vars names to suit your needs

  • In the pre_get_posts action, add whatever other parameters you need. See WP_Query for all available parameters

  • The custom rewrite rule sets post as the default post type, you can change it as needed. Also, the code uses default tags and categories, but can very easily be modified to make use of other taxonomies. I’ve tried to keep the code as general as possible to make customization as easy as possible

  • Most importantly, this code requires at least PHP 5.4. It will not work without modification on older versions. I’ve use a lot of closures which was only introduced in PHP 5.3. The other significant syntax is the use of short array tags ([]) which was introduced in PHP 5.4. I’ve used this in place of array()

THE CODE

/**
 * Function to get the categories and tags from the current single post
 *
 * @uses wp_get_post_terms()
 * @see http://codex.wordpress.org/Function_Reference/wp_get_post_terms
 *
 * The category and tag ID's are returned which will be used as values for the new query_vars
 *
 * @return (array) $query_string Array of category and tag ID's, each in string format
*/

function get_category_and_tag_ids() {

    /**
     * Get the current single post ID.
     *
     * @uses get_queried_object_id();
     * 
     * $post global is omitted as it is not reliable. Rather use get_queried_object_id()
     * See below link for a complete explanation
     *
     * @link https://wordpress.stackexchange.com/q/167706/31545
    */ 
    $post_id = get_queried_object_id();

    // Set the taxonomies to use, in this case category and post_tag
    $taxonomies = ['category', 'post_tag'];

    foreach( $taxonomies as $taxonomy ) {

            // Use wp_get_post_terms() to get the terms (categories and tags)
        $terms = wp_get_post_terms( $post_id, $taxonomy, ['orderby' => 'id', 'fields' => 'ids'] );

        if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {

            foreach( $terms as $term ) {

                if( $taxonomy == 'category' ) {

                    $categories[] = $term;

                }else{

                    $tags[] = $term;

                }

            }

        }

    }

    // Check if $categories is set, then convert the array into a comma separated string
    if( isset( $categories ) ) {

        $category_string_ids = implode( ',', $categories );

    }

    // Check if $tags is set, then convert the array into a comma separated string
    if( isset( $tags ) ) {

        $tag_string_ids = implode( ',', $tags );

    }

    // Returns $category_string_ids if it is set or empty string on failure 
    $category_query_string = ( isset( $category_string_ids ) ) ? $category_string_ids : '';

    // Returns $tag_string_ids if it is set or empty string on failure 
    $tag_query_string = ( isset( $tag_string_ids ) ) ? $tag_string_ids : '';

    //Create an array of category ids and tag ids to be returned
    $query_string = [
        'category_ids' => $category_query_string,
        'tags_ids' => $tag_query_string 
    ];

    return $query_string;

}

/**
 * Create a link to the virtual page which will be used to display the posts
 *
 * @uses get_home_url()
 * @return $permalink
 *
*/
function get_virtual_page_url() {

    $home_url = get_home_url();
    $slash="https://wordpress.stackexchange.com/";
    $virtual_page="custom-archive";
    $permalink = get_home_url() . $slash . $virtual_page . $slash;

    return $permalink;

}

/**
 * Register two new query variables cq, tq 
 *
 * @see http://codex.wordpress.org/WordPress_Query_Vars
 *
*/ 
add_filter( 'query_vars', function ( $qvars ) {

    $qvars[] = 'cq';
    $qvars[] = 'tq';

    return $qvars;

});

/**
 * Gets and sets the category and/or tag id as values to the custom query_vars
 *
 * Values are only set if they exists, if no value exists, the query_vars in not set
 *
 * @return $qv
*/
function get_query_vars_values() {

    $custom_query_values = get_category_and_tag_ids();
    $cat_values = $custom_query_values['category_ids'];
    $tag_values = $custom_query_values['tags_ids'];

    switch ( true ) {

        case ( $cat_values && $tag_values ):

            $qv = [
            'cq' => $cat_values,
            'tq' => $tag_values
            ]; 

            break;

        case ( $cat_values && !$tag_values ):

            $qv = [
            'cq' => $cat_values,
            ]; 

            break;

        default: 

            $qv = [];

            break;

    }

    return $qv;

}

/**
 * Build our link to the virtual page, custom-archive
 *
 * Sets the query_vars plus the respective values to the virtual page link
 *
 * @uses add_query_arg()
 * @see http://codex.wordpress.org/Function_Reference/add_query_arg
 *
 * Returns the link if we have values returned by get_query_vars_values()
 * Returns an empty string if get_query_vars_values() have no values
 * @return (string) $link 
*/
function get_create_link_virtual_page() {

    $get_virtual_page_link = get_virtual_page_url();
    $get_query_values = get_query_vars_values();

    $link = ( $get_query_values ) ? add_query_arg( $get_query_values, $get_virtual_page_link ) : '';

return $link;

}

/**
 * Wrapper function to echo the returned value set by get_create_link_virtual_page() 
 * Use display_virtual_page_link() in single.php if you need a plain link
*/
function display_virtual_page_link() {

    echo get_create_link_virtual_page();

}

/**
 * Special thanks to @Milo for the rewrite rule
 * @see https://wordpress.stackexchange.com/a/174243/31545
 *
 * Create a new rewrite rule so that WordPress can read interpret the page and adjust the main
 * query accordingly. This page will be rewritten to the homepage. Just a note, is_home() will
 * return true on this page. To counter act this, see function below
 *
 * @uses add_rewrite_rule()
 * @link http://codex.wordpress.org/Rewrite_API/add_rewrite_rule
 *
 * You should flush your permalinks for this to take effect or add flush_rewrite_rules()
 * inside a function that is hooked to 'after_switch_theme' or `register_activation_hook`
 * Never use flush_rewrite_rules() outside these two hooks, as it is a really expensive operation
*/ 
add_action( 'init', function () {

    add_rewrite_rule( 'custom-archive/?', 'index.php?post_type=post', 'top' );

});

/**
 * is_home() will return true on this page, and so might any other condition. This is annoying
 * as all functions meant for the home page will effect this custom page if you don't also check
 * for the custom query_vars
 *
 * To avoid this, set these conditions in the main query to false. This will avoid any extract
 * code and eliminate any unexpected output
 *
 * Set a new conditional tag, $wp_query->is_custom_archive if either of the two query_vars is present
 * @link http://codex.wordpress.org/Plugin_API/Action_Reference/parse_query
*/ 
add_filter( 'parse_query', function () {

    global $wp_query;

    //Set new conditional tag
    $wp_query->is_custom_archive = false;

    if( isset( $_GET['cq'] ) || isset( $_GET['tq'] ) ) {

        $wp_query->is_single = false;
        $wp_query->is_page = false;
        $wp_query->is_archive = false;
        $wp_query->is_search = false;
        $wp_query->is_home = false;
        $wp_query->is_custom_archive = true;

    }

}); 

/**
 * Wrapper function for $wp_query->is_custom_archive
 *
 * @return (bool) true on success, false on failure
*/ 
function is_custom_archive() {

    global $wp_query;
    return $wp_query->is_custom_archive;

}

/** 
 * Use a custom template to display our posts if needed. This can be omitted if you wish
 * In this case, index.php will be used if no template is set
 *
 * @uses template_include filter
 * @link http://codex.wordpress.org/Plugin_API/Filter_Reference/template_include
 *
 * @return $template
*/ 
add_filter( 'template_include', function ( $template ) {

    // Checks if we have one of the query_vars in the URL, if so, include custom_archive.php
    if( is_custom_archive() ) {

        $template = get_stylesheet_directory() . '/custom-archive.php';

    }
    return $template;

}, PHP_INT_MAX, 2 );

/**
 * Alter the main query on the custom archive page. We need to add a tax_query and get our custom
 * query_vars and use that in our tax query to get the correct posts
 *
 * @uses pre_get_posts action
 * @link http://codex.wordpress.org/Plugin_API/Action_Reference/pre_get_posts
 *
*/ 
add_action( 'pre_get_posts', function ( $q ) {

        if ( !is_admin() && is_custom_archive() && $q->is_main_query() ) {

            if( isset( $_GET['cq'] ) && isset( $_GET['tq'] ) ) {

                $tax_query =  [
                    [
                        'taxonomy'          => 'category',
                        'terms'             => $_GET['cq'],
                        'include_children'  => false,

                    ],
                    [
                        'taxonomy'  => 'post_tag',
                        'terms'     => $_GET['tq'],
                    ]
                ];

            }elseif( isset( $_GET['cq'] ) && !isset( $_GET['tq'] )) {

                $tax_query = [
                    [
                        'taxonomy'          => 'category',
                        'terms'             => $_GET['cq'],
                        'include_children'  => false,
                    ]
                ];

            }

            $q->set( 'tax_query', $tax_query );

        }

    return $q;

});

/**
 * Gets the current page URL. Will be used to set a 404 if http://mysite.com/custom-archive
 * is visited
 *
 * @uses get_home_url
 * @return (string) $current_url
*/
function get_current_page_url() {

    global $wp;
    $trail_slash="https://wordpress.stackexchange.com/";
    $current_url = home_url( add_query_arg( [], $wp->request ) ) . $trail_slash;

    return $current_url;

}

/**
 * Sets a 404 and redirects to a 404 page if http://mysite.com/custom-archive
 * is directly visited
*/ 
add_action( 'template_redirect', function () {

    global $wp_query; 

    if( !is_custom_archive() && get_current_page_url() == get_virtual_page_url() ) {

        $wp_query->set_404();
        status_header( 404 );
        nocache_headers();

    }

});

USAGE

To display your link in single.php you can do the following

<?php echo '<a href="' .get_create_link_virtual_page() . '">Custom link</a>'; ?>

Leave a Comment