#2行で
- 一覧ページ等で先頭固定表示に対応させるのは意外と大変
- pre_get_postsでのメインクエリ制御でなんとか実現したい
先頭固定表示とは?
標準で管理画面に実装されている機能で、指定した投稿を一番前に表示させたいために存在しています。
存在しているのですが、これにまともに向き合おうとすると結構大変です。
トップページとかの対応は楽
トップページに限らないのですが、メインクエリとは別のクエリを発行する箇所では、ちょっと意識するだけで対応可能です。
例えば、全記事から1件だけを雑に対応する場合は、こんな感じです。
$sticky = get_option('sticky_posts');
$disp_num = 3;
$rest_num = count($sticky) - $disp_num;
if($sticky){
$args = [
'posts_per_pages' => 1,
'post_in' => $sticky,
];
$the_query = new WP_Query( $args );
if ($the_query->have_posts()) : while ($the_query->have_posts()) : $the_query->the_post();
//ループ処理
endwhile;
endif;
wp_reset_postdata();
}
//残りの件数分($rest_num)、処理を回す
( ignore_sticky_posts => 1 と post__not_in は必要 )
表示させたい箇所のみ、クエリを発行する場合は、上記の対応で可能です。
一方、カテゴリ一覧等で対応しようとすると、一気に面倒になります。
ページ分割している場合、指定した件数以上の表示になる
例えば1ページ5件表示で、先頭固定表示を2件設定した場合、
最初のページだけ7件出てきます。
すっきりしないです。
2件以上設定すると、重複して再度表示される
設定した投稿が先頭に出てくるのですが、本来ある箇所にも表示されてしまいます。
推測なのですが、先頭固定表示の想定が1件だけなのかもと思います。
(でもarrayで取得出来るので、想定はしているハズ・・・)
メインクエリで解決したい
WP_Queryをcategory.phpとかに書いて、2ページ目以降の対応とかも記述すれば実現出来ますが、ちょっともやっとしますね。何よりメインクエリを破棄して対応するので、無駄が発生します。
なんとかメインクエリで解決したい。
と思って、以下のような記述だと(おそらく)動くと思います。
pre_get_postsの対応です。
//----------------------------------------------
// 出力件数設定
//----------------------------------------------
function change_posts_per_page($query) {
/* 管理画面,メインクエリに干渉しないために必須 */
if ( is_admin() || ! $query->is_main_query() ){
return;
}
if ( $query->is_home() || $query->is_category()) {
sticky_pre_get_posts($query);
return;
}
}
add_action( 'pre_get_posts', 'change_posts_per_page' );
functions.phpにまとめると長いので分割した場合
<?php
/**
* 先頭固定表示の対応、抽出して投稿IDを出力
*
* @param int 表示件数
* @param string カテゴリーを絞る場合は、カテゴリースラッグ
* @return array 投稿idの配列
*/
function show_top_page_post_ids(int $disp_num, $cat='')
{
$sticky = get_option('sticky_posts');
$args = [
'posts_per_page' => $disp_num,
'post__in' => $sticky,
];
if ($cat) {
$args['category_name'] = $cat;
}
$q = get_posts($args);
$post_ids = array_column($q, 'ID');//post_idだけを配列から取得
//投稿の中で、先頭固定にマッチする投稿を検索
$sticky_posts = [];
if ($post_ids) {
foreach ($post_ids as $post_s) {
if (in_array($post_s, $sticky, true)) {
$sticky_posts[] = $post_s;
}
}
}
$sticky_posts_num = count($sticky_posts);
//固定表示件数が表示件数と同数か、0ならreturn
if ($sticky_posts_num === $disp_num) {
return $sticky_posts;
}
//先頭固定表示がない場合は通常処理のpost_idsをreturn
if ($sticky_posts_num === 0) {
$args = [
'posts_per_page' => $disp_num,
];
if ($cat) {
$args['category_name'] = $cat;
}
$q = 0;
$q = get_posts($args);
if ($q) {
$post_ids = array_column($q, 'ID');//post_idだけを配列から取得
return $post_ids;
}
}
//先頭固定表示が表示件数未満の場合に、post_idを混ぜて返す
// $mix_post_num = $disp_num - $sticky_posts_num;
$args = [
'posts_per_page' => $disp_num,
'post__not_in' => $sticky_posts,
];
if ($cat) {
$args['category_name'] = $cat;
}
$q = get_posts($args);
$post_ids = "";
if ($q) {
$post_ids = array_column($q, 'ID');//post_idだけを配列から取得
$post_ids = array_merge($sticky_posts, $post_ids);//配列をマージ、先頭固定表示idを先頭に
return $post_ids;
}
}
/**
* 該当カテゴリーに先頭固定表示があるかを確認
*/
function is_exists_sticky_within_samecat(string $category_slug)
{
$sticky_posts = get_option('sticky_posts');
$args = [
'include' => $sticky_posts,
];
$posts = get_posts($args);
$flg = false;
foreach ($posts as $post) {
if (is_cat_match($post->ID, $category_slug)) {
$flg = true;
}
}
return $flg;
}
/**
* check category object
*/
function is_cat_match(int $post_id, string $category_slug)
{
$cat_obj = get_the_terms($post_id, 'category');
$flg = false;
foreach ($cat_obj as $cat) {
if ($cat->slug === $category_slug) {
$flg = true;
}
}
return $flg;
}
/**
* pre_get_posts対応
*/
function sticky_pre_get_posts($query)
{
//先頭固定表示がなければスルー
$post_num = get_option('posts_per_page');
$sticky_posts = get_option('sticky_posts');
if (!$sticky_posts) {
return;
}
//該当するカテゴリーに先頭固定表示がなければスルー
if (is_category()) {
$cat_slug = get_queried_object()->slug;
if (!is_exists_sticky_within_samecat($cat_slug)) {
return;
}
}
$query->set('ignore_sticky_posts', 1);
$query->set('posts_per_page', $post_num);
if (!is_paged()) {
if (is_home()) {
$post_ids = show_top_page_post_ids($post_num);
}
if (is_category()) {
$post_ids = show_top_page_post_ids($post_num, $cat_slug);
}
$query->set('post__in', $post_ids);
$query->set('orderby', 'post__in');
} else {
$sticky_count = count($sticky_posts);
$paged = get_query_var('paged') ? get_query_var('paged') : 1;
$offset_num = ($paged - 1) * $post_num - $sticky_count;
$query->set('offset', $offset_num);
$query->set('paged', $paged);
$query->set('post__not_in', $sticky_posts);
}
}
最初のページだけ先頭固定表示を含む投稿を表示させて、
残りは先頭固定表示を除いた投稿を表示させる記述です。
とりあえずこれで動くのですが、ページネーションがうまく動かないです。
メインクエリを書き換えているので当然ですね。
そのため、ページネーションを以下のような対応にします。
<?php
/*
cms_pagination
投稿一覧のページング
*/
function cms_pagination($pages = '', $range = 2)
{
global $paged;
// 文言の変更はここから
$paging_text['prev'] = '<';
$paging_text['next'] = '>';
$paging_text['newest'] = '最新';
$paging_text['oldest'] = '先頭';
// 文言の変更はここまで
if (empty($paged)) {
$paged = 1;
}
if (!isset($showitems)) {
$showitems = 0;
}
if ($pages == '') {
$pages = max_num_pages_with_sticky();
if (!$pages) {
$pages = 1;
}
}
$content = '';
if (1 != $pages) {
ob_start();
echo '<nav class="post-number">';
if ($paged > 1 && $showitems < $pages) {
echo '<a href="' . esc_url(get_pagenum_link($paged - 1)) . '">'.$paging_text['prev'].'</a>';
}
echo '<a href="'.esc_url(get_pagenum_link(1)).'">'.$paging_text['newest'].'</a>';
if ($paged > 2 && $paged > $range+1 && $showitems < $pages) {
echo '<a href="'.esc_url(get_pagenum_link(1)).'">«</a>';
}
for ($i = 1; $i <= $pages; $i++) {
if (1 != $pages && (!($i >= $paged + $range + 1 || $i <= $paged - $range - 1) || $pages <= $showitems)) {
echo ($paged == $i) ? '<span class="current">' . $i . '</span>' : '<a href="' . esc_url(get_pagenum_link($i)) . '" class="inactive">' . $i . '</a>';
}
}
echo '<a href="'.esc_url(get_pagenum_link($pages)).'">'.$paging_text['oldest'].'</a>';
if ($paged < $pages && $showitems < $pages) {
echo '<a href="' . esc_url(get_pagenum_link($paged + 1)).'">'.$paging_text['next'].'</a>';
}
echo '</nav>';
$content = ob_get_clean();
}
return $content;
}
/**
* 先頭固定表示を有効にした場合のために、
* pre_get_postsに影響されない最大ページ数を求める
*/
function max_num_pages_with_sticky()
{
$post_num = get_option('posts_per_page');
if(is_home()){
$count_publish_posts = wp_count_posts('post')->publish;
return ceil($count_publish_posts / $post_num);
}
if(is_category()){
$count_term = get_queried_object()->count;
return ceil($count_term / $post_num);
}
//上記以外はメインクエリの最大件数を返す
global $wp_query;
return $wp_query->max_num_pages;
}
ところどころコメントが適当だったりphpdocだったりするのは、ソースの継ぎ足しだったりするためです。
秘伝のソース大事。
カスタム投稿の対応は?
標準では対応せず、プラグインによる対応となります。
Seamless Sticky Custom Post Types
ただ、こちらのtracで議論されているように、みんなやりたいけど面倒、みたいな認識なんだろうと思います(適当)
先ほどのプラグインも5年間メンテされてないので利用は非推奨ですし、あまり積極的にサポートしない方が身のためかもです。