先頭固定表示を一覧ページでも実現する

#2行で
- 一覧ページ等で先頭固定表示に対応させるのは意外と大変
-  pre_get_postsでのメインクエリ制御でなんとか実現したい

# 先頭固定表示とは？
標準で管理画面に実装されている機能で、指定した投稿を一番前に表示させたいために存在しています。
存在しているのですが、これにまともに向き合おうとすると結構大変です。

# トップページとかの対応は楽
トップページに限らないのですが、メインクエリとは別のクエリを発行する箇所では、ちょっと意識するだけで対応可能です。
例えば、全記事から1件だけを雑に対応する場合は、こんな感じです。

```front-page.php
$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の対応です。

```functions.php
//----------------------------------------------
// 出力件数設定
//----------------------------------------------
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にまとめると長いので分割した場合

```fixed_post.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);
    }
}
```
最初のページだけ先頭固定表示を含む投稿を表示させて、
残りは先頭固定表示を除いた投稿を表示させる記述です。
とりあえずこれで動くのですが、ページネーションがうまく動かないです。
メインクエリを書き換えているので当然ですね。

そのため、ページネーションを以下のような対応にします。

```pagination.php
<?php
/*
  cms_pagination
    投稿一覧のページング
*/
function cms_pagination($pages = '', $range = 2)
{
   global $paged;
  // 文言の変更はここから
  $paging_text['prev'] = '&lt;';
  $paging_text['next'] = '&gt;';
  $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)).'">&laquo;</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](https://ja.wordpress.org/plugins/seamless-sticky-custom-post-types/)
ただ、こちらの[trac](https://core.trac.wordpress.org/ticket/12702)で議論されているように、みんなやりたいけど面倒、みたいな認識なんだろうと思います（適当）
先ほどのプラグインも5年間メンテされてないので利用は非推奨ですし、あまり積極的にサポートしない方が身のためかもです。