wordpress
投稿内の特定のキーワードに指定のリンクを自動で貼る
最終更新日: 2025年4月13日
MTならkotonohalinkというプラグインでできる。これは再構築高負荷だが、便利。特にSEO上内部リンクを増やしたいときに良いが、高負荷過ぎて再構築時500エラーやTimeOutを起こす可能性もある(経験済み)。
Wordpressの場合動的生成で都度DBをぐるっと回るので表示速度への影響はある。それが大きいか小さいかはDBのサイズとこのキーワード指定の数の多さと、サーバーの物理的なパフォーマンスに依る。
だから使用には最新の注意を。WPの場合通常トップページはテンプレに記述するからこの機能の埒外だが、固定ページをコンテンツのモジュールとしてトップに取り込むなんてことをすれば話は変わる。
PSIなどのツール使って確認しながら使いましょう。
後述で文字コードのことなどに触れているが、動くならあった方がいいかもね。
WEBマスターとかやってると時事Googleの検索アルゴリズムなどの変化に触れるが、ここ最近はユーザーの利便性と検索キーに対する結果の精度向上により重きを置いたようなので、やりっぱなしで済む施策で内部リンク増えるのはプラスでなかろうかと感じる。
悪用したりやり過ぎはUI低下やGoogleのペナ対象になるから気を付けましょう。
function my_autolink_keywords_in_paragraphs($content) {
$keywords = array(
'キーワード1' => 'リンク先のURL',
'キーワード2' => 'リンク先のURL',
'キーワード3' => 'リンク先のURL',
'キーワード4' => 'リンク先のURL',
'キーワード5' => 'リンク先のURL'
);
// <p>タグ内のテキストを抽出し、キーワードをリンクに変換
$new_content = preg_replace_callback('/<p>(.*?)<\/p>/is', function($matches) use ($keywords) {
$paragraph = $matches[1];
foreach ($keywords as $keyword => $url) {
// HTML エスケープに対応
$keyword_escaped = preg_quote($keyword, '/');
$pattern = '/('.$keyword_escaped.')/u';
$replacement = '<a href="' . esc_url($url) . '" class="totri">' . $keyword . '</a>';
$paragraph = preg_replace($pattern, $replacement, $paragraph);
}
return '<p>' . $paragraph . '</p>';
}, $content);
return $new_content;
}
add_filter('the_content', 'my_autolink_keywords_in_paragraphs');
概要
プラグインでこうしたものはあるが(使ってもいないけど)当スタジオは開発終了などの憂き目にあわないようPL使わない派なのでこの簡易なコードを作った。
$keywords array内は設定ページに表示されるためのinputとか書いてできるが、コードが冗長になるからfunction内弄る形です。
singleまたはpageの<p>タグ内に指定のキーワードがあった場合、preg_replaceでリンク付きのキーワードに置換するもの。
<P>内限定としたのは、hタグの行内にリンクがあると綺麗でないから。見出しに要らないでしょ。
$replacement変数指定部分のclass属性は好きに変えて、特に指定しないならこの部分消してもいい。
注意
キーワードの指定は正規表現を使っているが、「森のクマさん」「クマさん」といった指定をする場合は、処理順序をケアする必要がある。
それと文字コードにも注意。上記コードはUTF8対応だが文字コードの整合が取れないと正常に稼働しません。
修正版:上記コードでは内部ページの場合リンク先投稿内のキーワードにもリンクが貼られてしまうので、サイトの内部リンク対象限定で、keywordに対応するページのID指定しそのIDのページはこの処理から除外するようにした。
また既存のaタグ内にキーワードがあった場合そこにも働いてしまうので既存のaタグは除外するようにした。
function my_autolink_keywords_in_paragraphs($content) {
// キーワードとページID
$keywords_and_ids = array(
'森のクマさん' => 7404,
'クマさん' => 6332,
'動物園' => 6842
);
// 現在のページIDを取得
$current_page_id = get_the_ID();
// 現在のページが除外対象かどうかをチェック
if (in_array($current_page_id, $keywords_and_ids)) {
return $content;
}
// <a>タグ内の内容を一時的にプレースホルダーに置き換える
$placeholders = [];
$content = preg_replace_callback('/<a\b[^>]*>(.*?)<\/a>/is', function($matches) use (&$placeholders) {
$placeholder = '__PLACEHOLDER_' . count($placeholders) . '__';
$placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $content);
// <p>タグ内のテキストを抽出し、キーワードをリンクに変換
$content = preg_replace_callback('/<(p)>(.*?)<\/(p)>/is', function($matches) use ($keywords_and_ids) {
$tag = $matches[1]; // 'p' または 'li'
$content = $matches[2]; // タグ内の内容
foreach ($keywords_and_ids as $keyword => $page_id) {
$url = get_permalink($page_id);
$keyword_escaped = preg_quote($keyword, '/');
$pattern = '/('.$keyword_escaped.')/u';
$replacement = '<a href="' . esc_url($url) . '" class="totri">$1</a>';
$content = preg_replace($pattern, $replacement, $content);
}
return '<' . $tag . '>' . $content . '</' . $tag . '>';
}, $content);
// プレースホルダーを元の<a>タグに戻す
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
return $content;
}
add_filter('the_content', 'my_autolink_keywords_in_paragraphs');
コード=処理なのでできるだけ単純な方が良い、もちろんサーバーの物理的な性能にもよるが、表示の遅いページはそれだけでセキュリティリスクなどの心配も生じる。もちろん検索エンジンの評価も落ちる(多分コンテンツ>表示速度的なアルゴリズムだとは思うが)から早いに越したことはない。
プラグインならこのコードより優れたものもあるかもしれないが、重くなるのとPLの開発終了を嫌うとこうなる。
投稿時意図しないところにaが貼られるので場合によりレイアウトの崩れなどが起きるかもしれないが、CSSで調整するのが早いね。
変形・応用版
ある案件用に改変した。あくまでも参考程度に。
内部リンク(投稿ID指定)と外部リンク(URL指定)の混在
対象がpだけだったがliも対象に
除外キーワード設定(例:「クマさん」を指定しているが、クマさんを内包する「森のクマさん」にはリンクを貼りたくない時など)
function my_autolink_keywords_in_paragraphs($content) {
// 内部リンクのキーワードとページID
$internal_keywords_and_ids = array(
'クマさん' => 332,
'動物園' => 684
);
// 外部リンクのキーワードとURL
$external_keywords_and_urls = array(
'URL1' => 'https://URL1',
'URL2' => 'https://URL2'
);
//IDとURLのキーワードと処理順注意、ここでは家族葬を内包するキーのあるURLを先に処理
// 除外キーワードのリスト
$excluded_keywords = array(
'除外1',
'除外2'
);
// 現在のページIDを取得
$current_page_id = get_the_ID();
// 現在のページが内部リンクキーワードに対応するかチェック
if (in_array($current_page_id, $internal_keywords_and_ids)) {
return $content;
}
// <a>タグ内の内容を一時的にプレースホルダーに置き換える
$placeholders = [];
$content = preg_replace_callback('/<a\b[^>]*>(.*?)<\/a>/is', function($matches) use (&$placeholders) {
$placeholder = '__PLACEHOLDER_' . count($placeholders) . '__';
$placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $content);
// 外部リンクのキーワードを処理
$content = preg_replace_callback('/<(p)>(.*?)<\/(p)>/is', function($matches) use ($external_keywords_and_urls, $internal_keywords_and_ids, $excluded_keywords) {
$tag = $matches[1]; // 'p' または 'li'
$inner_content = $matches[2]; // タグ内の内容
// 除外キーワードが含まれているかチェック
$exclude = false;
foreach ($excluded_keywords as $excluded_keyword) {
if (strpos($inner_content, $excluded_keyword) !== false) {
$exclude = true;
break;
}
}
if (!$exclude) {
// 外部リンクのキーワードにリンクを適用
foreach ($external_keywords_and_urls as $keyword => $url) {
$keyword_escaped = preg_quote($keyword, '/');
$pattern = '/(' . $keyword_escaped . ')(?![^<]*<\/a>)/u'; // <a>タグ内を除外
$replacement = '<a href="' . esc_url($url) . '" class="totri">$1</a>';
$inner_content = preg_replace($pattern, $replacement, $inner_content);
}
// 内部リンクのキーワードにリンクを適用
foreach ($internal_keywords_and_ids as $keyword => $page_id) {
$url = get_permalink($page_id);
$keyword_escaped = preg_quote($keyword, '/');
$pattern = '/(' . $keyword_escaped . ')(?![^<]*<\/a>)/u'; // <a>タグ内を除外
$replacement = '<a href="' . esc_url($url) . '" class="totri">$1</a>';
$inner_content = preg_replace($pattern, $replacement, $inner_content);
}
}
return '<' . $tag . '>' . $inner_content . '</' . $tag . '>';
}, $content);
// プレースホルダーを元の<a>タグに戻す
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
return $content;
}
add_filter('the_content', 'my_autolink_keywords_in_paragraphs');
キーワード指定が複雑な場合はそれだけで正常稼働しない場合がある。特にキーワードを内包する他のキーワードや除外キーワードが絡み合うと自動リンクが貼られないことがある。
少しキーワードを参照するレベルの上下を試みたが結果は芳しくない、当該案件では稼働したのでこのままにした。
htmlタグは当初参照をp|liとして作ったがliのインラインにpがある場合にレイアウトが大崩れするので分けた。
全体に冗長になってきたので、動的生成には向かないかも。
環境だがMySQL5.7、php8.1、WP6.1up、テーマは当スタジオのオリジナル。テーマによりダメなケースもあるかもしれないが、その場合はデフォルトを親テーマとしてやれば動くかも。
指定キーワードを含んだ文字列の保護
「ホゲ」にリンクは貼りたいが、「ホゲモゴ」はそれで一つの語彙なので除外したい。
そんな例があったので修正。
function my_autolink_keywords_in_paragraphs($content) {
$internal_keywords_and_ids = array(
'ホゲ' => 9994,
'フガ' => 9995,
'モゲ' => 9996,
'モガ' => 9997
);
$external_keywords_and_urls = array(
'モゴモゴ' => 'https://ドメイン.com/ページ',
'ウゲウゲ' => 'https://ドメイン.com/ページ'
);
$excluded_keywords = array(
'ホゲモゴ',
'フガモゴ'
);
$excluded_page_ids = array(9998, 9999);
$current_page_id = get_the_ID();
// 現在のページIDが内部リンクのページIDリストや除外ページIDに含まれる場合、処理をスキップ
if (in_array($current_page_id, array_values($internal_keywords_and_ids)) ||
in_array($current_page_id, $excluded_page_ids)) {
return $content;
}
// 既存の<a>タグを保護
$placeholders = [];
$content = preg_replace_callback('/<a\b[^>]*>(.*?)<\/a>/is', function($matches) use (&$placeholders) {
$placeholder = '__PLACEHOLDER_' . count($placeholders) . '__';
$placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $content);
$process_tags = function($matches) use ($external_keywords_and_urls, $internal_keywords_and_ids, $excluded_keywords) {
$attributes = $matches[1];
$inner_content = $matches[2];
$tag = strpos($matches[0], '<li') === 0 ? 'li' : 'p';
// 除外キーワードを保護(最優先)
$exclude_placeholders = [];
foreach ($excluded_keywords as $index => $excluded_keyword) {
$placeholder = '__EXCLUDE_' . $index . '__';
$inner_content = str_replace($excluded_keyword, $placeholder, $inner_content);
$exclude_placeholders[$placeholder] = $excluded_keyword;
}
// 外部リンクと内部リンクをマージ
$all_keywords = array_merge(
array_map(function($keyword) use ($internal_keywords_and_ids) {
return ['keyword' => $keyword, 'url' => get_permalink($internal_keywords_and_ids[$keyword]), 'type' => 'internal'];
}, array_keys($internal_keywords_and_ids)),
array_map(function($keyword) use ($external_keywords_and_urls) {
return ['keyword' => $keyword, 'url' => $external_keywords_and_urls[$keyword], 'type' => 'external'];
}, array_keys($external_keywords_and_urls))
);
// キーワードを長さ順にソート(長いキーワードを優先)
usort($all_keywords, function($a, $b) {
return mb_strlen($b['keyword']) - mb_strlen($a['keyword']);
});
// リンク化処理
foreach ($all_keywords as $item) {
$keyword = $item['keyword'];
$url = $item['url'];
$keyword_escaped = preg_quote($keyword, '/');
// 除外キーワードに完全に一致する場合はスキップ
if (in_array($keyword, $excluded_keywords)) {
continue;
}
// リンク化処理(プレースホルダーを避ける)
$pattern = '/(' . $keyword_escaped . ')(?![^<]*<\/a>)(?!__EXCLUDE_\d+__)/u';
$replacement = '<a href="' . esc_url($url) . '" class="totri">$1</a>';
$inner_content = preg_replace($pattern, $replacement, $inner_content, 1);
}
// 除外キーワードを復元
foreach ($exclude_placeholders as $placeholder => $excluded_keyword) {
$inner_content = str_replace($placeholder, $excluded_keyword, $inner_content);
}
return '<' . $tag . $attributes . '>' . $inner_content . '</' . $tag . '>';
};
$content = preg_replace_callback('/<li([^>]*)>(.*?)<\/li>/is', $process_tags, $content);
$content = preg_replace_callback('/<p([^>]*)>(.*?)<\/p>/is', $process_tags, $content);
// 保護した<a>タグを復元
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
return $content;
}
add_filter('the_content', 'my_autolink_keywords_in_paragraphs');
これでより細密に指定ができるようになった。
UIの点でもSEOの点でも効果は不明(いや、単に未調査、面倒なんだ)、使いすぎると重くなるデメリットもあるが、軽くやっておいてもマイナスはないだろうね。
投稿/固定ページのタグの組み方によっては上記コードで不安定だったり「h」タグにリンクが貼られるケースもあるので微修正ver
function my_autolink_keywords_in_paragraphs($content) {
$internal_keywords_and_ids = array(
'ホゲ' => 9999,
'モゲ' => 9998
);
$external_keywords_and_urls = array(
'外部1' => 'https:///',
'外部2' => 'https:///'
);
$excluded_keywords = array(
'ホゲフガ',
'フガモゲ'
);
$excluded_page_ids = array(9998, 9999);
$current_page_id = get_the_ID();
// 現在のページIDが除外ページIDに含まれる場合、処理をスキップ
if (in_array($current_page_id, array_values($internal_keywords_and_ids)) ||
in_array($current_page_id, $excluded_page_ids)) {
return $content;
}
// <h1>-<h6>タグを保護
$heading_placeholders = [];
$content = preg_replace_callback('/<h[1-6]([^>]*)>(.*?)<\/h[1-6]>/is', function($matches) use (&$heading_placeholders) {
$placeholder = '__HEADING_PLACEHOLDER_' . count($heading_placeholders) . '__';
$heading_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $content);
// 既存の<a>タグを保護
$a_placeholders = [];
$content = preg_replace_callback('/<a\b[^>]*>(.*?)<\/a>/is', function($matches) use (&$a_placeholders) {
$placeholder = '__A_PLACEHOLDER_' . count($a_placeholders) . '__';
$a_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $content);
$process_tags = function($matches) use ($external_keywords_and_urls, $internal_keywords_and_ids, $excluded_keywords) {
$attributes = $matches[1];
$inner_content = $matches[2];
$tag = strpos($matches[0], '<li') === 0 ? 'li' : 'p';
// <b>, <strong>, <span>, <em>, <i> などのサブタグを保護
$subtag_placeholders = [];
$inner_content = preg_replace_callback('/<(b|strong|span|em|i)([^>]*)>(.*?)<\/\1>/is', function($matches) use (&$subtag_placeholders) {
$placeholder = '__SUBTAG_PLACEHOLDER_' . count($subtag_placeholders) . '__';
$subtag_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $inner_content);
// 除外キーワードを保護
$exclude_placeholders = [];
foreach ($excluded_keywords as $index => $excluded_keyword) {
$placeholder = '__EXCLUDE_' . $index . '__';
$inner_content = str_replace($excluded_keyword, $placeholder, $inner_content);
$exclude_placeholders[$placeholder] = $excluded_keyword;
}
// 外部リンクと内部リンクをマージ
$all_keywords = array_merge(
array_map(function($keyword) use ($internal_keywords_and_ids) {
return ['keyword' => $keyword, 'url' => get_permalink($internal_keywords_and_ids[$keyword]), 'type' => 'internal'];
}, array_keys($internal_keywords_and_ids)),
array_map(function($keyword) use ($external_keywords_and_urls) {
return ['keyword' => $keyword, 'url' => $external_keywords_and_urls[$keyword], 'type' => 'external'];
}, array_keys($external_keywords_and_urls))
);
// キーワードを長さ順にソート
usort($all_keywords, function($a, $b) {
return mb_strlen($b['keyword']) - mb_strlen($a['keyword']);
});
// リンク化処理
foreach ($all_keywords as $item) {
$keyword = $item['keyword'];
$url = $item['url'];
$keyword_escaped = preg_quote($keyword, '/');
// リンク化処理(既存リンクとプレースホルダーを避ける)
$pattern = '/(' . $keyword_escaped . ')(?![^<]*<\/a>)(?!__EXCLUDE_\d+__)(?!__SUBTAG_PLACEHOLDER_\d+__)/u';
$replacement = '<a href="' . esc_url($url) . '" class="totri">$1</a>';
$inner_content = preg_replace($pattern, $replacement, $inner_content, 1);
}
// 除外キーワードを復元
foreach ($exclude_placeholders as $placeholder => $excluded_keyword) {
$inner_content = str_replace($placeholder, $excluded_keyword, $inner_content);
}
// サブタグを復元
foreach ($subtag_placeholders as $placeholder => $original) {
$inner_content = str_replace($placeholder, $original, $inner_content);
}
return '<' . $tag . $attributes . '>' . $inner_content . '</' . $tag . '>';
};
// <p> と <li> のみを処理
$content = preg_replace_callback('/<p([^>]*)>(.*?)<\/p>/is', $process_tags, $content);
$content = preg_replace_callback('/<li([^>]*)>(.*?)<\/li>/is', $process_tags, $content);
// <a>タグを復元
foreach ($a_placeholders as $placeholder => $original) {
$content = str_replace($placeholder, $original, $content);
}
// <h1>?<h6>タグを復元
foreach ($heading_placeholders as $placeholder => $original) {
$content = str_replace($placeholder, $original, $content);
}
return $content;
}
add_filter('the_content', 'my_autolink_keywords_in_paragraphs', 10);
※多分サーバーのメモリ食うだろうし、使いすぎると重くなるし、案件により不具合の可能性がある。
一応pとli内に限定はしているが、内容により保証はないので不具合発生は個別に修正するのがベター。