WordPress SEO: Programmatic Internal Linking with Related Posts and Link Suggestions

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.