I’m creating quite complex website for a literary magazine. For its custom post types Issue and Event, I wanted different permalink structure than for posts (which is basic http://example.com/%postname%).
My task wasn’t the easy one and I’ve managed to come up with a solution, even though it is a quite twisted one. As I’m not sure if that’s ideal, I would love to hear opinions of WP users, who understand the dark heart of internal $wp_query
better than I do. If WP Stackexchange is not the appropriate place for a code review, please refer me somewhere else.
I needed to achieve the following:
- URLs reflect the dates relevant to CPTs – such as http://example.com/event/%eventyear%/%eventmonth%/%postname% and http://example.com/event/%year%/%issue-num%
- The last part of URL can be duplicated – such as http://example.com/issue/2016/5 and http://example.com/issue/2015/5
- CPT have archives – archive of 2012 issues will be available on http://example.com/issue/2012.
- The last part of URL should be generated from something else than the title (meta value perhaps).
First, I’ve registered my CPT:
$cpt_args = array(
'label' => 'Issue',
'hierarchical' => false,
'has_archive' => true,
'rewrite' => array('slug' => 'issue/%issueyear%'),
);
Then, I’ve registered my rewrite tags:
add_action('init', function() use ($tags) {
add_rewrite_tag("%issueyear%");
});
Then, I’ve registered my “rewrite_tag translation” of `%issueyear% to custom meta value:
$func = function($permalink, $post) {
if (strpos($permalink, "%issueyear%")) {
$tags_used[] = $t;
}
$issue_start = get_post_meta($post->ID, 't_issue_start', true);
$old = basename($permalink);
$new = $this->date->get_time('Y', $issue_start);
$permalink = str_replace($old, $new, $permalink);
return $permalink;
};
add_action('post_link', $func, 10, 2);
add_action('post_type_link', $func, 10, 2);
Then, I’ve added my rewrite rules, passing my meta value to URL.
add_action('init', function() {
add_rewrite_rule(
"^issue/([0-9]{4})/([0-9]{1,})/?",
'index.php?post_type=issue&year=$matches[1]&cibulka_key=cibulka_slug&cibulka_val=$matches[2]',
'top'
);
};
add_action('query_vars', function($vars) {
$vars[] = 'cibulka_key';
$vars[] = 'cibulka_val';
return $vars;
});
URL http://example.com/2016/5
would give me archive.php template. So there, I included single template, if $wp_query
has cibulka_key
set. include_template_with_var
is my function, allowing passing parameters to templates.
/** Archive.php */
global $wp_query;
if (isset($wp_query->query['cibulka_key'])) {
$meta_value = $wp_query->query['cibulka_val'] . '_' . $wp_query->query['year'];
$args = array(
'post_type' => $wp_query->query['post_type'],
'meta_key' => $wp_query->query['cibulka_key'],
'meta_value' => $meta_value,
'posts_per_page' => 1
);
$posts = get_posts($args);
if (!empty($posts)) {
$data = array(
'id' => $posts[0]->ID,
'post' => $posts[0]
);
} else {
$data = array();
}
include_template_with_var('single.php', $data);
} else {
// Do normal archive stuff
}
Single template of post with $data['id']
is served instead of archive.php.
Note: This is not my actual code, I’ve simplified it a lot for the purpose of my question.
This achieves all 4 points, that I needed achieved, I’m just not sure if it’s the right way to approach this though – performance-wise (there will be A LOT of events), standard-wise, etc. So before I stick with this sollution (as it will quite heavy consequences, as setting up URL scheme has), I would like to hear some oppinions.
So far, I’ve discovered those caveats:
- WP recognizes content on http://example.com/issue/5 as an
archive
, even though it should besingle
. Sois_single()
,is_single('issue')
returnfalse
and the result ofbody_class()
is confusing.
I will add them here as more of them appear.
Thanks a lot in advance!
Edit 1
I’ve removed conditionals from Archive.php and replaced it with filters. This solved caveat 1 and removed logic from my templates. Yay!
// Change global `wp_query` to retrieving the post by meta query AND mark it as single (not archive)
add_action('pre_get_posts', function() {
global $wp_query;
if (!isset($wp_query->query['cibulka_key'])) { return; }
switch ($wp_query->query['post_type']) {
case 'issue':
$meta_value = $wp_query->query['cibulka_val'] . '_' . $wp_query->query['year'];
break;
}
$wp_query->set('meta_key', $wp_query->query['cibulka_key']);
$wp_query->set('meta_value', $meta_value);
$wp_query->is_singular = true;
$wp_query->is_single = true;
$wp_query->is_archive = false;
remove_all_actions ( '__after_loop');
});
// Use single.php rather than archive.php, if global `$wp_query` contains my custom properties
add_filter('template_include', function($template) {
global $wp_query;
if (!isset($wp_query->query['cibulka_key'])) { return $template; }
$single_tmplt = locate_template('single.php');
return $single_tmplt;
});
For some reason body_class()
does not add single-issue
CSS class this way, but that is nothing …
add_filter('body_class', function($body_class) {
if (is_single() && get_post_type() === 'issue') {
$body_class[] = 'single-issue';
}
return $body_class;
}, 11, 1);
… can’t fix. 🙂