WordPressサイトの記事下部に配置する「前の記事・次の記事」ナビゲーション。
この記事では、WP_Query を使って 「順序(menu_order)」または「公開日」で並び順を指定できる 「前の記事・次の記事」ナビゲーションを、カスタムテーマ向けに実装する方法を解説します。PHP・CSSのフルコード付きです。
完成イメージ


- 記事ページ下部に、前の記事・次の記事を PC横並び/SP縦並び で表示
- サムネイル、タイトル、「前の記事」「次の記事」ラベルを配置
- タイトルは指定文字数を超えたら 末尾を
…で省略 (CSSでも複数行クランプ) - 前後の記事がない場合は 「記事がありません」 をグレーで表示(リンク無効)
- 設定変数ひとつ で並び順(順序 or 日付)を切り替え可能
標準の get_previous_post() では不十分
WordPress の get_previous_post() / get_next_post() は、 投稿日(post_date)を基準に前後の記事を判定 します。シンプルなブログ運用ならこれで十分ですが、実案件では次のような弱点が問題になります。
- 既存記事を複製して新記事を作った場合、
post_dateが古いまま引き継がれると、公開順とは違う位置に前後リンクがつながる - 「順序(
menu_order)」で手動並び替えしたい要件に対応できない - 並び順のカスタマイズ余地がほとんどない
そこで、WP_Query で同じ投稿タイプの記事を任意の並び順で取得し、その配列の中で現在記事の前後を割り出す方式に置き換えます。
実装コード(フルソース)
以下を template-parts/prevnextPost.php などのファイル名で保存し、single.php から get_template_part() で呼び出します。
<style>
/* prevnextPost:CSS(前の記事・次の記事)
────────────── ༚༶⊰⟡⊱༶༚༺*/
.prextPost.prextPost-container {
margin-top: 2.5rem;
width: 728px;
max-width: 100%;
height: auto;
margin-inline: auto;
}
.prextPost .prextPost-wrapper {
width: 100%;
}
.prextPost a {
display: inline-block;
width: 100%;
height: 100%;
background: var(--c-secondary-20, hsl(224, 25%, 98%));
}
/* hover */
@media (any-hover: hover) {
.prextPost [href]:hover {
background: var(--c-secondary-100, hsl(224, 25%, 93%));
opacity: 1;
}
}
.prextPost [class*="prextPost-slide"] {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: auto;
border: 1px var(--c-base-300, hsl(224, 6%, 74%)) solid;
}
.prextPost .prextPost-term {
flex-shrink: 0;
width: 5.625rem;
height: 5.625rem;
margin-top: auto;
background: var(--c-base-40, hsl(224, 6%, 96%));
}
.prextPost .prextPost-term img {
width: 100%;
height: 100%;
object-fit: cover;
}
.prextPost .prextPost-data {
flex-grow: 1;
padding-inline: 1pc;
text-align: center;
}
.prextPost .prextPost-title {
font-size: clamp(0.8750rem, calc(0.8750rem + ((1vw - 3.6px) * 0.1563)), 0.9375rem);
font-weight: 500;
line-height: 1.5;
text-align: center;
color: var(--c-base-900, hsl(224, 6%, 13%));
}
/* リンク無効 */
.prextPost a.hasnt-post {
pointer-events: none;
cursor: none;
}
.prextPost .prextPost-badge {
padding-block: 7px;
font-size: 14px;
font-weight: 600;
line-height: 1;
}
.prextPost a.hasnt-post .prextPost-title {
color: var(--c-base-300, hsl(224, 6%, 74%));
}
/* prextPost:CSS @(SP) */
@media screen and (max-width: 743.9px) {
.prextPost .prextPost-slide-next { margin-top: 2.5rem; }
.prextPost .prextPost-badge {
position: absolute;
top: -3em;
left: 0px;
margin: auto;
color: var(--c-base-900, hsl(224, 6%, 13%));
}
.prextPost .prextPost-badge::before {
position: relative;
top: 7px;
display: inline-flex;
place-content: center;
place-items: center;
margin-left: -5px;
font-size: 178.3%;
line-height: 1;
font-family: 'Material Symbols Sharp';
font-variation-settings: 'FILL' 1, 'wght' 300;
color: var(--c-primary, hsl(224, 50%, 33%));
}
.prextPost .prextPost-slide-prev .prextPost-badge::before { content: "\e045"; }
.prextPost .prextPost-slide-next .prextPost-badge::before { content: "\e044"; }
.prextPost .prextPost-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* @(PC) */
@media print, screen and (min-width: 744px) {
.prextPost .prextPost-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.prextPost [class*="prextPost-slide"] { height: 7.5pc; }
.prextPost .prextPost-slide-next {
flex-direction: row-reverse;
border-left: none;
}
.prextPost .prextPost-badge {
position: absolute;
margin: auto;
width: 5.625rem;
text-align: center;
background: var(--c-primary, hsl(224, 50%, 33%));
color: #FFF;
}
.prextPost .prextPost-slide-prev .prextPost-badge { top: 0px; left: 0px; }
.prextPost .prextPost-slide-next .prextPost-badge { top: 0px; right: 0px; }
.prextPost .prextPost-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>
<!-- ✿prevnextPost.php(前の記事・次の記事) -->
<?php
/**
* タイトルを指定文字数で切り詰める
*/
function trim_post_title($title, $max_len = 40, $suffix = '…')
{
if (mb_strlen($title) > $max_len) {
return mb_substr($title, 0, $max_len) . $suffix;
}
return $title;
}
/**
* 現在の記事の前後の記事を、指定した並び順で取得する
*/
function get_adjacent_posts_by_order($current_id, $orderby = 'date', $order = 'DESC')
{
$post_type = get_post_type($current_id);
// pre_get_posts の影響を避けるため WP_Query を直接使う
$query = new WP_Query([
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => $orderby,
'order' => $order,
'fields' => 'ids',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
$ids = $query->posts;
$index = array_search($current_id, $ids, true);
$prev_id = ($index !== false && isset($ids[$index + 1])) ? $ids[$index + 1] : null;
$next_id = ($index !== false && isset($ids[$index - 1])) ? $ids[$index - 1] : null;
return [
'prev' => $prev_id ? get_post($prev_id) : null,
'next' => $next_id ? get_post($next_id) : null,
];
}
/**
* ナビゲーション表示用の生データを返す(HTMLは組み立てない)
*/
function get_post_navigation_data($post, $max_title_length = 40, $default_title = '記事がありません')
{
if (!$post) {
return [
'url' => '',
'title' => $default_title,
'thumbnail' => '',
'disabled' => true,
];
}
$raw_title = strip_tags(get_the_title($post->ID));
if (has_post_thumbnail($post->ID)) {
$thumbnail = get_the_post_thumbnail($post->ID, 'thumbnail');
} else {
$noimage_url = get_theme_file_uri('assets/images/default-image.png');
$thumbnail = '<img src="' . esc_url($noimage_url) . '" alt="NoImage" />';
}
return [
'url' => get_permalink($post->ID),
'title' => trim_post_title($raw_title, $max_title_length),
'thumbnail' => $thumbnail,
'disabled' => false,
];
}
// ─── ■設定 ───────────── ༚༶⊰⟡⊱༶༚༺
// タイトルの最大文字数
$max_title_length = 40;
// 前後記事の並び順
// 'menu_order' : 「順序」カラムで並べる
// 'date' : 公開日で並べる
$prevnext_orderby = 'date';
// 並び方向($ids が「新→古」順になる方向を指定)
// menu_order で「順序1が最新」の運用なら 'ASC'
// date なら 'DESC'
$prevnext_order = 'DESC';
// ───────────────────── ༚༶⊰⟡⊱༶༚༺──
// 前後の記事を取得
$adjacent = get_adjacent_posts_by_order(get_the_ID(), $prevnext_orderby, $prevnext_order);
// 表示用データ
$prev = get_post_navigation_data($adjacent['prev'], $max_title_length);
$next = get_post_navigation_data($adjacent['next'], $max_title_length);
?>
<?php if ($adjacent['prev'] || $adjacent['next']) : ?>
<nav id="prextPostClbP" class="prextPost prextPost-container">
<div class="prextPost-wrapper">
<!-- 前の記事 -->
<a <?php if ($prev['disabled']) : ?>class="hasnt-post" <?php else : ?>href="<?php echo esc_url($prev['url']); ?>"<?php endif; ?>>
<dl class="prextPost-slide-prev">
<dt class="prextPost-term"><?php echo $prev['thumbnail']; ?></dt>
<dd class="prextPost-data">
<b class="prextPost-badge">前の記事</b>
<em class="prextPost-title"><?php echo esc_html($prev['title']); ?></em>
</dd>
</dl>
</a>
<!-- 次の記事 -->
<a <?php if ($next['disabled']) : ?>class="hasnt-post" <?php else : ?>href="<?php echo esc_url($next['url']); ?>"<?php endif; ?>>
<dl class="prextPost-slide-next">
<dt class="prextPost-term"><?php echo $next['thumbnail']; ?></dt>
<dd class="prextPost-data">
<b class="prextPost-badge">次の記事</b>
<em class="prextPost-title"><?php echo esc_html($next['title']); ?></em>
</dd>
</dl>
</a>
</div>
</nav>
<?php endif; ?>
コード解説
1. タイトルを文字数で切り詰める trim_post_title()
function trim_post_title($title, $max_len = 40, $suffix = '…')
{
if (mb_strlen($title) > $max_len) {
return mb_substr($title, 0, $max_len) . $suffix;
}
return $title;
}
日本語タイトルに対応するため、必ず mb_strlen() / mb_substr() を使用します。strlen() / substr() はマルチバイト文字を1バイト単位で切ってしまうため、末尾が文字化けする原因になります。文字数上限を引数で受け取れるようにしておくと、テーマ側から柔軟に指定できます。
2. WP_Query で並び順を指定して前後の記事を取得
$query = new WP_Query([
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => $orderby,
'order' => $order,
'fields' => 'ids',
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
ポイントは次の3つ。
get_posts()ではなくnew WP_Query()を直接使用:テーマや関連プラグインがpre_get_postsフィルタでposts_per_pageを書き換えているケースがあり、get_posts()だと影響を受けて全件取れないことがあります。WP_Queryを直接呼ぶことで、こうした介入を受けにくくなります。fields => 'ids'とupdate_*_cache => false:ID配列だけを返し、メタ/タームキャッシュ生成もスキップして軽量化。no_found_rows => true:ページネーション用のカウントクエリを実行せず高速化。
取得した $ids は「新しい→古い」順に並んでいる想定で、
[$index + 1]= より古い記事 = 前の記事[$index - 1]= より新しい記事 = 次の記事
として抽出しています。
なお posts_per_page => -1 は全件取得のため、投稿数が数千件を超える規模では Transients などでのキャッシュを検討してください。
3. HTMLを組み立てず「素データ」を返す get_post_navigation_data()
以前は $link = '<a href="...">'; のように 部分タグを文字列として返す 設計にしていましたが、テンプレート側で <a> と </a> がペアで見えなくなり、エディタが構文エラーを出したりフォーマッタが誤動作したりする原因になっていました。
今回は URL・タイトル・サムネイル・無効フラグを配列で返す設計にし、テンプレートは通常のHTMLとして開始タグと終了タグを完全な形で書けるようにしています。
4. テンプレート側の分岐
<a <?php if ($prev['disabled']) : ?>class="hasnt-post" <?php else : ?>href="<?php echo esc_url($prev['url']); ?>"<?php endif; ?>>
disabled フラグで 属性だけを切り替える 形にすることで、<a>...</a> のタグ構造をテンプレート上に常に完全な形で残せます。
5. セキュリティ処理
esc_url()、esc_html()、strip_tags() の3点はセットで使います。特にWordPressでは投稿タイトルに <br> などのHTMLタグが埋め込まれるケースがあり、strip_tags() を挟まないとナビゲーション表示で意図しない改行が入ります。
カスタマイズ設定
ファイル下部の設定ブロックで、3つのポイントを制御できます。
// タイトルの最大文字数
$max_title_length = 40;
// 並び順の基準
// 'menu_order' : 「順序」カラム(手動並び替え)
// 'date' : 公開日
$prevnext_orderby = 'date';
// 並び方向($ids が「新→古」順になる方向)
// menu_order で「順序1が最新」の運用なら 'ASC'
// date なら 'DESC'
$prevnext_order = 'DESC';
「順序」で並べたい場合
管理画面のカスタム並び替えプラグイン等で menu_order を手動設定している運用なら、次のように切り替えるだけで対応できます。
$prevnext_orderby = 'menu_order';
$prevnext_order = 'ASC'; // 順序=1 が最新の場合
公開日で並べたい場合(従来通り)
$prevnext_orderby = 'date';
$prevnext_order = 'DESC';
single.php への組み込み
投稿ページのループ内で get_template_part() を使って呼び出します。
<?php get_header(); ?>
<main>
<?php if (have_posts()) : ?>
<?php while (have_posts()) : the_post(); ?>
<article>
<h1><?php the_title(); ?></h1>
<div class="post-content">
<?php the_content(); ?>
</div>
</article>
<!-- 前の記事・次の記事 -->
<?php get_template_part('template-parts/prevnextPost'); ?>
<?php endwhile; ?>
<?php endif; ?>
</main>
<?php get_footer(); ?>
まとめ
この実装のポイントを整理すると次の通りです。
WP_Queryベース でget_previous_post()の弱点(pre_get_postsの影響を受ける/並び順が固定)を回避- 並び順を「順序(menu_order)」「公開日」から選択可能
- タイトルの文字数制限 (PHPで切り詰め+CSSで
-webkit-line-clamp)で長文タイトルにも対応 - HTMLを組み立てず素データを返す設計 で、テンプレート・エディタ・フォーマッタの相性が良い
nav+dlの意味付け と、 CSSclamp()によるレスポンシブフォント で軽量に仕上げ
「前の記事・次の記事」ナビゲーションを、「公開日」で並べたいという方は試してみてください。