2026年5月23日
2026年5月23日
WordPressで内部リンクを自動化する方法|プラグインと自作コードの使い分け
WordPressの内部リンクを自動化するには、「関連記事の候補を提案してもらう」方法と「決めたキーワードに自動でリンクを付ける」方法の2系統があります。記事数が数百件を超えたら、提案型のプラグインで質を保ちつつ、定番の用語だけをキーワード自動リンクで補うのが現実的です。この記事では、両者の違い、プラグインを選ぶときの観点、データベースを書き換えずに表示時だけリンクを足す自作コード、そして自動化で失敗しやすい点まで説明します。
内部リンク自動化の2つの方式
提案型:候補を出し、人が選んで挿入する
投稿の本文を解析して「この段落からこの記事へリンクできそうです」と候補を出すタイプです。Link Whisper、Yoast SEO Premium の内部リンク提案機能、Rank Math のリンク提案などがこの系統にあたります。最終的に人が採否を決めるので、文脈に合わないリンクが入りにくいのが長所です。反面、1記事ずつ編集画面を開く必要があるため、既存記事を一気に整備する用途では時間がかかります。
キーワード型:語句とURLの対応表から自動で付ける
「パーマリンク設定」という語が出てきたらパーマリンクの解説記事へリンクする、という対応表を作り、該当する語句に自動でリンクを付けるタイプです。Internal Link Juicer や Link Whisper の自動リンク機能などがあり、後述のように自作もできます。数千記事に一括で効かせられる一方、文脈を読まないので、同じ語でも別の意味で使っている箇所にまでリンクが付くことがあります。
どちらか一方に決める必要はありません。用語集のように意味がぶれない語はキーワード型、記事同士の文脈的なつながりは提案型、と役割を分けると管理しやすくなります。
プラグインを選ぶときの確認点
各プラグインの機能や無料版・有料版の範囲はアップデートで変わるため(2026年10月現在の情報として)、導入前に公式ページで次の点を確かめてください。
- リンクの保存方式:投稿本文(データベース)に直接
<a>タグを書き込むのか、表示時にだけ差し込むのか。書き込み型はプラグインを停止してもリンクが残り、表示型は停止すると消えます。 - 1記事あたりの上限:同じキーワードを何回までリンクするか、1記事に何本まで付けるかを設定できるか。
- 除外の指定:見出し、既存のリンク内、特定の投稿やカテゴリを除外できるか。
- レポート機能:被リンクが0本の「孤立記事」を一覧できるか。自動化の効果測定にはこれが一番役立ちます。
- 日本語の扱い:日本語は単語の区切りに空白がないため、英語圏のプラグインでは語の境界判定がうまく働かないことがあります。テスト環境で日本語記事に試してから本番に入れるのが安全です。
書き込み型のプラグインで一括処理をかける場合は、実行前にデータベースのバックアップを取ってください。数千件の投稿本文が一度に書き換わるため、意図しない置換が起きたときに戻す手段が必要です。
自作コードで表示時にキーワードリンクを付ける
プラグインを増やしたくない場合や、リンク先を厳密に管理したい場合は、the_content フィルターで表示時にリンクを差し込む方法があります。データベースの本文は変更しないので、コードを外せば元に戻ります。
ここで気をつけたいのは、HTML全体に対して単純な文字列置換をしないことです。既存の <a> タグの中や href 属性の値、見出しやコードブロックの中まで置換すると、リンクの入れ子やHTMLの破損が起きます。下のコードはHTMLをタグとテキストに分け、除外タグの外にあるテキストだけを対象にしています。
<?php
/**
* Plugin Name: Simple Auto Internal Links
* Description: 対応表のキーワードに、表示時だけ内部リンクを付ける。
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* キーワード => リンク先の投稿ID
*/
function sail_get_keyword_map() {
$map = array(
'パーマリンク設定' => 123,
'子テーマ' => 456,
);
return apply_filters( 'sail_keyword_map', $map );
}
add_filter( 'the_content', 'sail_auto_link', 20 );
function sail_auto_link( $content ) {
if ( is_admin() || is_feed() || ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}
$map = sail_get_keyword_map();
if ( empty( $map ) ) {
return $content;
}
// 長いキーワードを先に処理する(「WordPressテーマ」を「WordPress」より優先)
uksort(
$map,
function ( $a, $b ) {
return mb_strlen( $b ) - mb_strlen( $a );
}
);
$current_id = get_the_ID();
$max_links = 3; // 1記事に付ける最大本数
$added = 0;
$used = array(); // 同じリンク先は1回だけ
$links = array(); // プレースホルダー => <a>タグ
$skip_tags = array( 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code', 'script', 'style', 'button', 'figcaption' );
$depth = 0;
$parts = preg_split( '/(<[^>]+>)/u', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
foreach ( $parts as $i => $part ) {
if ( '' === $part ) {
continue;
}
// タグ部分:除外タグの開始・終了だけを数える
if ( '<' === $part[0] ) {
if ( preg_match( '#^<(/?)([a-z0-9]+)#i', $part, $m ) && in_array( strtolower( $m[2] ), $skip_tags, true ) ) {
$depth = ( '/' === $m[1] ) ? max( 0, $depth - 1 ) : $depth + 1;
}
continue;
}
if ( $depth > 0 || $added >= $max_links ) {
continue;
}
foreach ( $map as $keyword => $post_id ) {
$post_id = (int) $post_id;
if ( $added >= $max_links ) {
break;
}
if ( isset( $used[ $post_id ] ) || $post_id === $current_id ) {
continue;
}
$pos = mb_strpos( $part, $keyword );
if ( false === $pos || 'publish' !== get_post_status( $post_id ) ) {
continue;
}
$token = "\x00" . count( $links ) . "\x00";
$matched = mb_substr( $part, $pos, mb_strlen( $keyword ) );
$links[ $token ] = '<a href="' . esc_url( get_permalink( $post_id ) ) . '">' . $matched . '</a>';
$part = mb_substr( $part, 0, $pos ) . $token . mb_substr( $part, $pos + mb_strlen( $keyword ) );
$used[ $post_id ] = true;
$added++;
}
$parts[ $i ] = $part;
}
return strtr( implode( '', $parts ), $links );
}
コードの要点を順に見ていきます。
- 適用範囲の絞り込み:
is_singular( 'post' )とin_the_loop()、is_main_query()で、投稿の個別ページのメインコンテンツだけに効かせています。ウィジェットや抜粋、フィードにリンクが混ざるのを防ぐためです。 - 優先度20:ブロックエディタの本文は優先度9の
do_blocksでHTMLに変換されます。それより後に実行することで、ブロックのコメントではなく最終的なHTMLを相手にできます。 - プレースホルダー:挿入した
<a>タグを一時的に記号へ置き換えておき、最後にまとめて戻しています。こうしないと、後続のキーワードが挿入済みのリンク文字列にさらにマッチしてしまいます。 - リンク文字列:リンクのテキストには本文に元々あった文字列をそのまま使います。本文は出力済みのHTMLなので、ここで再度エスケープすると
&が二重になるなどの崩れが起きます。URLはesc_url()を通しています。 - 自己リンクと非公開記事の除外:表示中の記事自身や、下書き・非公開の記事にはリンクしません。
対応表が増えてきたら、配列を直接書き換える代わりに sail_keyword_map フィルターで別ファイルから読み込むと、運用担当者がコードに触らずに済みます。
動作確認の方法
有効化したら、対応表のキーワードを含む記事を開き、次の点を確かめます。
- ブラウザの開発者ツールで、リンクが本文の段落内にだけ付いているか(見出し・目次・コード例に付いていないか)
- 既存のリンクの中に
<a>が入れ子になっていないか - 1記事あたりのリンク数が設定した上限に収まっているか
- ページキャッシュを使っている場合は、キャッシュを削除してから確認しているか
キャッシュ系プラグインが入っていると、コードを変えても古いHTMLが表示され続けます。確認がうまくいかないときは、まずキャッシュを疑ってください。
自動化でつまずきやすい点
部分一致による誤リンク
日本語は語の区切りが曖昧なので、「プラグイン」をキーワードにすると「プラグイン開発」「プラグインの競合」など、あらゆる場所にマッチします。キーワードは「キャッシュプラグイン」のように、なるべく具体的で長い語にしてください。上のコードでは長い語を先に処理しますが、それでも短い汎用語は避けるのが基本です。
リンクの付けすぎ
1記事に大量の自動リンクが並ぶと、読者にとってはどれが重要なリンクか分からなくなります。検索エンジンに対しても、本文と関係の薄いリンクが多い状態は評価の助けになりません。1記事あたり2〜3本程度から始め、Search Console やアクセス解析で回遊の変化を見ながら調整するのが無難です。
アンカーテキストの単調さ
キーワード型は、どの記事からも同じアンカーテキストでリンクされる状態になりがちです。主要な記事ほど、提案型や手作業で文脈に合った言い回しのリンクも混ぜると、自然なリンク構造になります。
リンク先の変更
スラッグの変更やURL統合(301リダイレクト)が起きても、投稿IDで管理していれば get_permalink() が新しいURLを返すため、リンク切れになりません。URL文字列で対応表を作ると、移転のたびに対応表の修正が必要になります。投稿IDで持つ方式をおすすめする理由はここにあります。
効果を確認する
自動化した後は、内部リンクの状況を定期的に点検します。Google Search Console の「リンク」レポートでは、内部リンクが多く集まっているページの一覧を確認できます。逆に、重要なのに内部リンクがほとんど集まっていないページがあれば、提案型ツールや手作業でリンクを追加する対象になります。
もう一つの目安は、被リンク0本の孤立記事の数です。孤立記事はクローラーに発見されにくく、読者が回遊でたどり着くこともありません。自動化の前後で孤立記事の数を比べると、施策の効果が数字で分かります。
まとめ
- 内部リンクの自動化には、候補を提案する「提案型」と、対応表から自動で付ける「キーワード型」がある
- 意味がぶれない用語はキーワード型、文脈的なつながりは提案型と役割を分けると管理しやすい
- プラグインは、リンクを本文に書き込むか表示時に差し込むか、上限と除外を設定できるか、日本語で正しく動くかを確認して選ぶ
- 自作する場合は
the_contentフィルターで、既存リンク・見出し・コードの中を避けて表示時だけ差し込むと、データベースを汚さずに済む - キーワードは具体的で長い語にし、1記事あたりの本数を抑え、リンク先は投稿IDで管理する
- 書き込み型で一括処理する前にはデータベースをバックアップする