Internal linking is one of the highest-ROI on-page SEO improvements: it distributes PageRank across your site, signals topic clusters to crawlers, and keeps readers on-site longer. For sites with hundreds of posts, manual linking is impractical. A programmatic approach — using MySQL FULLTEXT similarity to find related posts and injecting links into content via a filter — scales without editorial overhead.
Problem: A large WordPress site has dozens of topically related posts that are not linked to each other — Google can't discover the internal link structure, and readers have no contextual path to related content.
Solution: Build a programmatic internal linking system: for each published post, extract keywords from post_title and tags, query related posts with a FULLTEXT search or a meta-based similarity score, then inject links into the post content at render time using the the_content filter. Cache the related post list as a transient to avoid repeated queries.
The code below adds a "Related Posts" section after the content using FULLTEXT similarity, automatically inserts in-content links to related posts for key phrases, and caches the results per post to avoid repeated queries.
esc_like( $post->post_title ) . ' '
. $wpdb->esc_like( wp_strip_all_tags( substr( $post->post_content, 0, 200 ) ) );
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT ID, post_title, post_name,
MATCH(post_title, post_content) AGAINST (%s IN NATURAL LANGUAGE MODE) AS score
FROM {$wpdb->posts}
WHERE post_status = 'publish'
AND post_type = 'post'
AND ID != %d
AND MATCH(post_title, post_content) AGAINST (%s IN NATURAL LANGUAGE MODE)
ORDER BY score DESC
LIMIT %d",
$search, $post_id, $search, $limit
) );
wp_cache_set( $cache_key, $results, 'seo_internal_links', 6 * HOUR_IN_SECONDS );
return $results;
}
// 2. Append related-posts list after single post content
add_filter( 'the_content', function ( string $content ): string {
if ( ! is_single() || ! in_the_loop() ) {
return $content;
}
$related = get_related_posts_by_content( get_the_ID(), 4 );
if ( ! $related ) {
return $content;
}
$html = '';
return $content . $html;
} );
NOTE: Ensure the wp_posts table has a FULLTEXT index on (post_title, post_content) before using this approach — without it MySQL will fall back to a full table scan on every page load, which will be far slower than the cache benefit provides.