Auto Interlinking for WordPress [set and forget]

Was looking to make this code secure and without vulnerability of any sort using Qwen

Can you confirm now the code is more secure?? or have I broken it. P.S. i am not a coder or technically sound
Thanks, will look into it.
Great share, thanks!
Your welcome.

Auto interlinking can be a great time-saver for WordPress websites, as it helps with internal linking, which is essential for SEO. By automatically linking related content, you can improve website navigation and distribute link equity effectively. However, it's important to monitor the links regularly to ensure relevance and avoid any potential SEO issues. Using high-quality plugins for auto interlinking, like 'Link Whisper,' can help optimize this process.
Is it just me, or does this sound like an AI replying to my post title? :D

This is cool! The best way I found was using screaming frog crawl + python script to create an embedding map for each post (based on context of the page) and then link that way.

If you developed this into an actual plugin, I think you could input openAI API key and have it do that. The links would likely be a lot more contextual too!
It is meant to be simple. There is a gap in the market where you can actually make this into a plugin and use AI, but I am good with what this one does, as are probably 90% of people as well :)
 
Thanks for this gem man. Just wondering, as I am a noob in this game, how do I actually interlink old posts etc. I have set it up properly. Just don't know how to implement. Any help would be much appreciated.
 
Thanks for this gem man. Just wondering, as I am a noob in this game, how do I actually interlink old posts etc. I have set it up properly. Just don't know how to implement. Any help would be much appreciated.
I mentioned how to do it before, but since I have not much to do atm, I can repeat myself again.

If you set it up correctly, and by that, I mean you:

1. Installed the code snippet plugin
2. Added my snippet
3. Went to Auto interlinking settings (under settings tab at wp admin menu)
4. Added your desired sitemaps there

Then, all you need to do is to go to "Posts" from the WordPress admin menu, select a maximum of 20 posts (old ones) and then do bulk edit in the edit window find "Status" setting, swap from "- No Change -" to published and then save. Do the same thing for all other old posts of yours, hence, you could do more posts per time, but that depends on your server capabilities. In your case, I would do 10-20.
 
It legit takes you max 10 min (if even that), but I perfectly understand that sometimes you just can't be bothered :D
uhh
it does not
maybe it's just me but i like to do extensive testing before i add any new script, code or plugin or at the very least make someone i trust do it for me lol
 
uhh
it does not
maybe it's just me but i like to do extensive testing before i add any new script, code or plugin or at the very least make someone i trust do it for me lol
It's a good habit to have at the end of the day :) got no such patience myself :D
 
Hmm came to this point where i made it into a plugin for ease of use in a way? :D

But I have 2 ways to handle interlinking now:

First one.

AJAX interlinking:

Here's where keywords are saved and how they're used:

  1. Where they are saved:
    • The keywords (along with their corresponding URLs, forming a "keyword map") are saved as a single entry in your standard WordPress options table in the database (wp_options).
    • The specific option name used to store them is sitemap_interlinker_keyword_map (defined by the SITEMAP_INTERLINKER_OPTION_KEYWORDS constant in the code). This is a standard way for plugins to store data. You wouldn't normally need to look at this directly in the database.
  2. How they are used:
    • The plugin uses the function sitemap_interlinker_add_links_to_content, which is hooked into WordPress's the_content filter.
    • When a post is viewed on your website: This function automatically runs.
    • It fetches the saved keyword map: It retrieves the list of 3176 (thats a sample number from my test) keywords and their associated URLs from the sitemap_interlinker_keyword_map option you just saved.
    • It reads the post's content: It takes the text of the post being viewed.
    • It searches for keywords: It carefully scans through the post text, looking for exact matches of the keyword phrases stored in the map. It prioritizes longer keywords first.
    • It creates links: When it finds a keyword in the text and adding a link respects the rules you set in the settings (like "Max Links per Post", "Max Links per Paragraph", "Sentences to Skip at Start", and ensuring the same keyword or URL isn't linked multiple times in one post), it automatically converts that keyword text into an HTML link (<a> tag), pointing to the URL associated with that keyword in your saved map.
    • Dynamic Process: This happens dynamically each time a post is loaded for viewing on the frontend of your site. The original post content stored in the database isn't permanently changed; the links are added on-the-fly during page display.
So, now that the keyword map is saved, the plugin should start automatically adding relevant internal links to your posts when people visit them, based on the keywords found and the rules you configured!

Then the big questions are:

  1. Will it slow down blog post loading/site speed?
    • Yes, potentially. The process of adding links runs on your server every time a single post or page is loaded by a visitor.
    • This process involves: fetching the saved keyword list (potentially thousands of keywords), reading the entire post content, parsing the HTML structure (using DOMDocument), searching through the text for all those keywords, applying the linking rules you set, and then rebuilding the HTML with the links inserted.
    • Impact: Doing this work on every page load willadd some processing time compared to just displaying the static content. The exact impact depends on:
      • Server Performance: How fast your web server's CPU is.
      • Post Length/Complexity: Longer posts take more time to parse and search.
      • Keyword List Size: Searching for more keywords takes more time (you have over 3000).
      • Caching: Good server-side caching (like object caching or full-page caching) can significantly reduce the impact for repeat visitors, but the first visit to a page after changes will always involve this processing.
    • Recommendation: Keep an eye on your site's loading speed after implementing this. If you notice a significant slowdown, you might need to consider optimizing server resources or exploring more advanced caching strategies.
  2. Are the links consistent or do they change on refresh?
    • The links added should be consistent for the same post each time it's refreshed.
    • Why:The plugin uses a deterministic process:
      • It always starts with the same original post content.
      • It always uses the exact same keyword map that you saved.
      • It searches for keywords in a predictable order (longest first).
      • It applies the rules (max links, max per paragraph, skip sentences, uniqueness) in the same way every time.
    • The links won't randomly change places or disappear between page loads unless you go back to the settings page, re-scan the sitemaps (potentially generating a different keyword map), or change the linking rules and save the settings.
In summary: expect a potential performance trade-off for the automatic linking functionality, but the links themselves should be stable and predictable for any given post.

Personally, I haven't noticed any slowdown of the site or extensive resource usage on the server. But need to check more.



Second one.

Direct link saving:



We can change the plugin to save the links directly into the post content in the database when you save or update a post, instead of adding them dynamically every time someone views the post.

Advantages:

  • Faster Frontend Loading: This will likely improve your site's loading speed for visitors, as the link insertion processing won't need to happen on every page view.
  • Links Visible in Editor: The links will become part of the actual post content, so you'll see them in the WordPress editor.
Disadvantages / Trade-offs:

  • Slower Post Saving: The processing work shifts from page view time to post save time. Saving or updating posts (especially long ones with many potential keywords) might take slightly longer in the admin area.
  • Manual Updates Required: If you update your keyword map later (by re-scanning sitemaps), the links in existing posts will not automatically update. The links will only be re-evaluated and potentially changed when you manually edit and save that specific post again.
  • "Permanent" Links: If you deactivate or delete the plugin later, the links that were saved into your posts will remain there. They won't be automatically removed.


So not too sure what rout to take :D
 
Interlinking is important for ranking these days. I’ll try this one!
 
So here is a small gift for WordPress bloggers who don't care that much about how much related article is to be interlinked as long as it's somewhat related (probably perfect for the auto bloggers cooking shit left and right like me).
What it can do for the most part while it's not perfect, it does create easy setting access under the settings tab in admin:

View attachment 434094
View attachment 434095

This interlinking strategy follows these guidelines:
  • Links are generally not placed in headings.
  • Single-keyword anchor texts are avoided; the aim is typically 2-3 word keywords.
  • The same anchor text (keyword phrase) is not used more than once within a single post.
  • The same destination URL is not used for different anchor texts within the same post.
Also not tested but you can create sitemap URLs beforehand, etc if you have less than 200 blogs, all of them fit in sitemap.xml, but if you cross 200 posts, you get a second post sitemap2.xml etc so my point is you can add sitemap1, sitemap2, sitemap3 before hand so when they gonna be there it would use them for you not even having to remember to add them.

Also, I added some stop words that should be ignored and not taken into consideration when adding links.


If anyone wants to improve this further, be my guest, but please share it here with the community.


To use this Auto Interlinker, all you need is a simple code snippet plugin "WPCode" or "Code Snippets" are perfect.


PHP:
// ===========================================
// Constants and Defaults
// ===========================================
define('MY_INTERLINKING_OPTION_NAME', 'my_interlinking_options'); // Key to store settings in wp_options

function my_interlinking_get_defaults() {
    // Default values for the settings
    return [
        'sitemap_urls'              => '', // Stores newline separated URLs
        'max_links'                 => 6,
        'max_per_paragraph'         => 1,
        'cache_expiry'              => 6 * HOUR_IN_SECONDS,
        'min_sentences_to_skip'     => 3,
    ];
}

// ===========================================
// Stop Word Definition
// ===========================================
function my_interlinking_get_stop_words() {
    // Returns the array of stop words
    return [
        'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'aren\'t', 'as', 'at', 'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by', 'can\'t', 'cannot', 'could', 'couldn\'t', 'did', 'didn\'t', 'do', 'does', 'doesn\'t', 'doing', 'don\'t', 'down', 'during', 'each', 'few', 'for', 'from', 'further', 'had', 'hadn\'t', 'has', 'hasn\'t', 'have', 'haven\'t', 'having', 'he', 'he\'d', 'he\'ll', 'he\'s', 'her', 'here', 'here\'s', 'hers', 'herself', 'him', 'himself', 'his', 'how', 'how\'s', 'i', 'i\'d', 'i\'ll', 'i\'m', 'i\'ve', 'if', 'in', 'into', 'is', 'isn\'t', 'it', 'it\'s', 'its', 'itself', 'let\'s', 'me', 'more', 'most', 'mustn\'t', 'my', 'myself', 'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own', 'same', 'shan\'t', 'she', 'she\'d', 'she\'ll', 'she\'s', 'should', 'shouldn\'t', 'so', 'some', 'such', 'than', 'that', 'that\'s', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'there\'s', 'these', 'they', 'they\'d', 'they\'ll', 'they\'re', 'they\'ve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very', 'was', 'wasn\'t', 'we', 'we\'d', 'we\'ll', 'we\'re', 'we\'ve', 'were', 'weren\'t', 'what', 'what\'s', 'when', 'when\'s', 'where', 'where\'s', 'which', 'while', 'who', 'who\'s', 'whom', 'why', 'why\'s', 'with', 'won\'t', 'would', 'wouldn\'t', 'you', 'you\'d', 'you\'ll', 'you\'re', 'you\'ve', 'your', 'yours', 'yourself', 'yourselves'
    ];
}


// ===========================================
// Settings Page Setup Functions
// ===========================================

/** Add admin menu */
function my_interlinking_add_admin_menu() {
    add_options_page('Auto Interlinking Settings', 'Auto Interlinking', 'manage_options', 'my_interlinking_options_page', 'my_interlinking_options_page_html');
}

/** Register settings */
function my_interlinking_settings_init() {
    register_setting('my_interlinking_settings_group', MY_INTERLINKING_OPTION_NAME, 'my_interlinking_options_sanitize');
    add_settings_section('my_interlinking_main_section', 'Main Settings', 'my_interlinking_section_callback', 'my_interlinking_options_page');
    add_settings_field('sitemap_urls', 'Sitemap URLs (One per line)', 'my_interlinking_render_sitemap_urls_field', 'my_interlinking_options_page', 'my_interlinking_main_section');
    add_settings_field('max_links', 'Max Links per Post', 'my_interlinking_render_max_links_field', 'my_interlinking_options_page', 'my_interlinking_main_section');
    add_settings_field('max_per_paragraph', 'Max Links per Paragraph', 'my_interlinking_render_max_per_paragraph_field', 'my_interlinking_options_page', 'my_interlinking_main_section');
    add_settings_field('min_sentences_to_skip', 'Sentences to Skip at Start', 'my_interlinking_render_min_sentences_field', 'my_interlinking_options_page', 'my_interlinking_main_section');
    add_settings_field('cache_expiry', 'Sitemap Cache Expiry', 'my_interlinking_render_cache_expiry_field', 'my_interlinking_options_page', 'my_interlinking_main_section');
}

/** Section callback */
function my_interlinking_section_callback() { echo '<p>Configure the settings for the automatic internal linking process.</p>'; }

/** Render Sitemap URLs Textarea */
function my_interlinking_render_sitemap_urls_field() { $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults()); $value = isset($options['sitemap_urls']) ? $options['sitemap_urls'] : ''; ?> <textarea rows="5" cols="50" class='regular-text' name='<?php echo MY_INTERLINKING_OPTION_NAME; ?>[sitemap_urls]'><?php echo esc_textarea($value); ?></textarea> <p class="description">Enter one full XML Sitemap URL per line. Required.</p> <?php }
/** Render Max Links Input */
function my_interlinking_render_max_links_field() { $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults()); $value = isset($options['max_links']) ? $options['max_links'] : my_interlinking_get_defaults()['max_links']; ?> <input type='number' step='1' min='0' max='20' class='small-text' name='<?php echo MY_INTERLINKING_OPTION_NAME; ?>[max_links]' value='<?php echo esc_attr($value); ?>'> <p class="description">Maximum number of internal links per post (0-20).</p> <?php }
/** Render Max Per Paragraph Input */
function my_interlinking_render_max_per_paragraph_field() { $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults()); $value = isset($options['max_per_paragraph']) ? $options['max_per_paragraph'] : my_interlinking_get_defaults()['max_per_paragraph']; ?> <input type='number' step='1' min='1' max='5' class='small-text' name='<?php echo MY_INTERLINKING_OPTION_NAME; ?>[max_per_paragraph]' value='<?php echo esc_attr($value); ?>'> <p class="description">Maximum number of links allowed in a single paragraph (1-5).</p> <?php }
/** Render Min Sentences Input */
function my_interlinking_render_min_sentences_field() { $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults()); $value = isset($options['min_sentences_to_skip']) ? $options['min_sentences_to_skip'] : my_interlinking_get_defaults()['min_sentences_to_skip']; ?> <input type='number' step='1' min='0' max='10' class='small-text' name='<?php echo MY_INTERLINKING_OPTION_NAME; ?>[min_sentences_to_skip]' value='<?php echo esc_attr($value); ?>'> <p class="description">Sentences at start where links should NOT be added (0-10).</p> <?php }
/** Render Cache Expiry Select */
function my_interlinking_render_cache_expiry_field() { $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults()); $current_value = isset($options['cache_expiry']) ? $options['cache_expiry'] : my_interlinking_get_defaults()['cache_expiry']; $expiry_options = [ '3600' => '1 Hour', '10800' => '3 Hours', '21600' => '6 Hours', '43200' => '12 Hours', '86400' => '1 Day', '604800' => '1 Week' ]; ?> <select name='<?php echo MY_INTERLINKING_OPTION_NAME; ?>[cache_expiry]'><?php foreach ($expiry_options as $seconds => $label) : ?><option value="<?php echo esc_attr($seconds); ?>" <?php selected($current_value, $seconds); ?>><?php echo esc_html($label); ?></option><?php endforeach; ?></select><p class="description">How long sitemap data should be cached.</p> <?php }

/** Sanitize options */
function my_interlinking_options_sanitize($input) {
    $sanitized_input = []; $defaults = my_interlinking_get_defaults();
    $valid_urls = [];
    if (isset($input['sitemap_urls'])) { $potential_urls = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $input['sitemap_urls']))); foreach ($potential_urls as $url) { $sanitized_url = esc_url_raw($url); if (!empty($sanitized_url)) { $valid_urls[] = $sanitized_url; } } }
    $sanitized_input['sitemap_urls'] = implode("\n", $valid_urls);
    $sanitized_input['max_links'] = isset($input['max_links']) ? min(20, absint($input['max_links'])) : $defaults['max_links']; $sanitized_input['max_per_paragraph'] = isset($input['max_per_paragraph']) ? max(1, min(5, absint($input['max_per_paragraph']))) : $defaults['max_per_paragraph']; $sanitized_input['min_sentences_to_skip'] = isset($input['min_sentences_to_skip']) ? min(10, absint($input['min_sentences_to_skip'])) : $defaults['min_sentences_to_skip']; $valid_expiries = [3600, 10800, 21600, 43200, 86400, 604800]; $sanitized_input['cache_expiry'] = isset($input['cache_expiry']) && in_array(intval($input['cache_expiry']), $valid_expiries) ? intval($input['cache_expiry']) : $defaults['cache_expiry'];
    return $sanitized_input;
}

/** Render options page HTML */
function my_interlinking_options_page_html() { if (!current_user_can('manage_options')) return; ?> <div class="wrap"><h1><?php echo esc_html(get_admin_page_title()); ?></h1><form action="options.php" method="post"><?php settings_fields('my_interlinking_settings_group'); do_settings_sections('my_interlinking_options_page'); submit_button('Save Settings'); ?></form></div> <?php }


// ===========================================
// Core Interlinking Logic Functions
// ===========================================

/**
 * Fetches, parses, merges, and caches data from multiple sitemaps.
 * @return array|null Combined array of [url => slug] pairs or null on failure.
 */
function my_get_sitemap_data() {
    // Multi-sitemap fetch logic from V2.7/V2.8
    $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults());
    $sitemap_urls_string = isset($options['sitemap_urls']) ? $options['sitemap_urls'] : '';
    $cache_expiry = isset($options['cache_expiry']) ? absint($options['cache_expiry']) : (6 * HOUR_IN_SECONDS);
    $sitemap_urls = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $sitemap_urls_string)));
    if (empty($sitemap_urls)) return null;

    $cache_key_part = implode(',', $sitemap_urls);
    $cache_key = 'my_interlinking_sitemap_data_v2_multi_' . md5($cache_key_part);
    $cached_data = get_transient($cache_key);
    if (false !== $cached_data) return $cached_data;

    $combined_sitemap_links = []; $fetch_errors = 0;
    foreach ($sitemap_urls as $sitemap_url) {
        $response = wp_remote_get($sitemap_url, ['timeout' => 15]);
        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { error_log('Auto Interlinking Error: Failed to fetch sitemap (' . $sitemap_url . '). ' . (is_wp_error($response) ? $response->get_error_message() : 'Code: ' . wp_remote_retrieve_response_code($response))); $fetch_errors++; continue; }
        $xml_body = wp_remote_retrieve_body($response); if (empty($xml_body)) { error_log('Auto Interlinking Error: Sitemap body is empty (' . $sitemap_url . ').'); $fetch_errors++; continue; }
        libxml_use_internal_errors(true); $sitemap_xml = simplexml_load_string($xml_body); $xml_errors = libxml_get_errors(); libxml_clear_errors(); libxml_use_internal_errors(false); if (false === $sitemap_xml) { error_log('Auto Interlinking Error: Failed to parse sitemap XML (' . $sitemap_url . '). Errors: ' . print_r($xml_errors, true)); $fetch_errors++; continue; }
        if (isset($sitemap_xml->url)) { foreach ($sitemap_xml->url as $url_entry) { if (isset($url_entry->loc)) { $url = (string)$url_entry->loc; $url_normalized = rtrim(esc_url($url), '/'); $path = trailingslashit(parse_url($url_normalized, PHP_URL_PATH)); $slug = basename($path); if (!empty($url_normalized) && !empty($slug) && !isset($combined_sitemap_links[$url_normalized])) { $combined_sitemap_links[$url_normalized] = strtolower(str_replace('-', ' ', $slug)); } } } } elseif(isset($sitemap_xml->sitemap)) { error_log('Auto Interlinking Info: Sitemap index file (' . $sitemap_url . ') found. Sub-parsing not implemented.'); } else { error_log('Auto Interlinking Warning: No <url> or <sitemap> elements found in (' . $sitemap_url . ').'); }
    } // End foreach
    if ($fetch_errors < count($sitemap_urls) && !empty($combined_sitemap_links)) { set_transient($cache_key, $combined_sitemap_links, $cache_expiry); } elseif (empty($combined_sitemap_links) && $fetch_errors == count($sitemap_urls)) { error_log('Auto Interlinking Error: Failed to get any links from provided sitemaps.'); return null; }
    return $combined_sitemap_links;
}

/**
 * Check if a phrase contains stop words.
 */
function my_interlinking_contains_stop_words_v2( $phrase, $stop_words, $post_id_debug = 0 ) {
    // Logic confirmed working in user's V2.6 reference
    if ( empty( $stop_words ) ) return false; $words = preg_split( '/\s+/u', strtolower( $phrase ) );
    foreach ( $words as $word ) { $trimmed_word = trim( $word, " \t\n\r\0\x0B.,?!():;\"'" ); if ( ! empty( $trimmed_word ) ) { if ( in_array( $trimmed_word, $stop_words, true ) ) { return true; } } }
    return false;
}

/** Count sentences */
function my_interlinking_count_sentences($text) { return preg_match_all('/[.?!](\s+|$)/u', $text); }


/**
 * Main Interlinking Function using DOM V2.9
 * (Logic identical to user's working V2.6 reference)
 */
function my_auto_interlink_post_dom($post_id, $post) {
    // Safety checks
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; if (wp_is_post_autosave($post_id)) return; if (!in_array($post->post_status, ['publish', 'future'])) return; if (isset($_POST['prev_status']) && !in_array($_POST['prev_status'], ['publish', 'draft', 'pending', 'private', 'future'])) return; if ($post->post_type !== 'post') return;

    // Get Settings
    $options = get_option(MY_INTERLINKING_OPTION_NAME, my_interlinking_get_defaults());
    $max_links = isset($options['max_links']) ? absint($options['max_links']) : 6;
    $max_per_p = isset($options['max_per_paragraph']) ? absint($options['max_per_paragraph']) : 1;
    $skip_sentences_threshold = isset($options['min_sentences_to_skip']) ? absint($options['min_sentences_to_skip']) : 3;
    if ($max_links <= 0) return;

    // Get Data (uses the multi-sitemap function)
    $sitemap_data = my_get_sitemap_data();
    $stop_words_list = my_interlinking_get_stop_words();
    if (empty($sitemap_data)) return;
    $current_post_url_normalized = rtrim(get_permalink($post_id), '/'); if (isset($sitemap_data[$current_post_url_normalized])) { unset($sitemap_data[$current_post_url_normalized]); } if (empty($sitemap_data)) return;

    // Prepare Content & Keywords (with stop word check)
    $original_content = $post->post_content; if (strlen($original_content) < 100) return;
    $text_content = wp_strip_all_tags($original_content); $text_content = preg_replace('/\s+/', ' ', $text_content); $words = preg_split('/\s+/u', $text_content); $potential_keywords = []; $word_count = count($words);
    for ($i = 0; $i < $word_count; $i++) {
        if ($i + 1 < $word_count) { $phrase2 = trim($words[$i] . ' ' . $words[$i + 1]); if (strlen($phrase2) > 5 && !is_numeric(substr($phrase2, 0, 1)) && !my_interlinking_contains_stop_words_v2($phrase2, $stop_words_list, $post_id)) { $potential_keywords[strtolower($phrase2)] = $phrase2; } }
        if ($i + 2 < $word_count) { $phrase3 = trim($words[$i] . ' ' . $words[$i + 1] . ' ' . $words[$i + 2]); if (strlen($phrase3) > 8 && !is_numeric(substr($phrase3, 0, 1)) && !my_interlinking_contains_stop_words_v2($phrase3, $stop_words_list, $post_id)) { $potential_keywords[strtolower($phrase3)] = $phrase3; } }
    }
    if (empty($potential_keywords)) return;
    $shuffled_keywords = array_keys($potential_keywords); shuffle($shuffled_keywords);

    // DOM Setup
    $links_added = 0; $used_urls = []; $used_keywords = []; $modified = false; $links_per_paragraph = [];
    $dom = new DOMDocument(); libxml_use_internal_errors(true); if (!$dom->loadHTML('<?xml encoding="UTF-8">' . $original_content, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED )) { libxml_clear_errors(); return; } libxml_clear_errors(); libxml_use_internal_errors(false);
    $xpath = new DOMXPath($dom); $text_nodes = $xpath->query('//text()[normalize-space(.) != ""][not(ancestor::h1) and not(ancestor::h2) and not(ancestor::h3) and not(ancestor::h4) and not(ancestor::h5) and not(ancestor::h6) and not(ancestor::a) and not(ancestor::script) and not(ancestor::style)]');
    if ($text_nodes === false || $text_nodes->length === 0) return;
    $sentences_processed = 0;

    // Main Processing Loop (Identical logic to your V2.6 reference)
    foreach ($text_nodes as $node) {
        if ($links_added >= $max_links) break; $node_text = $node->nodeValue;
        $process_this_node = false; if ($sentences_processed >= $skip_sentences_threshold) { $process_this_node = true; } else { $sentences_in_node = my_interlinking_count_sentences($node_text); $sentences_processed += $sentences_in_node; if ($sentences_processed >= $skip_sentences_threshold) { $process_this_node = true; } } if (!$process_this_node) continue;
        $current_p_node = $xpath->query('ancestor::p[1]', $node)->item(0); $p_hash = $current_p_node ? spl_object_hash($current_p_node) : 'no_paragraph'; $count_in_p = isset($links_per_paragraph[$p_hash]) ? $links_per_paragraph[$p_hash] : 0; if ($count_in_p >= $max_per_p) continue;
        $node_modified_in_loop = false;
        foreach ($shuffled_keywords as $lower_keyword) {
            if ($node_modified_in_loop || $links_added >= $max_links) break;
            if (in_array($lower_keyword, $used_keywords, true)) continue; // Skip used keyword
            $original_case_keyword = $potential_keywords[$lower_keyword]; $position = mb_stripos($node_text, $original_case_keyword, 0, 'UTF-8');
            if ($position !== false) {
                $char_before = ($position > 0) ? mb_substr($node_text, $position - 1, 1, 'UTF-8') : ' '; $char_after_pos = $position + mb_strlen($original_case_keyword, 'UTF-8'); $char_after = ($char_after_pos < mb_strlen($node_text, 'UTF-8')) ? mb_substr($node_text, $char_after_pos, 1, 'UTF-8') : ' ';
                if (preg_match('/\W|^\s*$/u', $char_before) && preg_match('/\W|^\s*$/u', $char_after)) { // Word boundary
                    foreach ($sitemap_data as $url => $slug) {
                        if (in_array($url, $used_urls, true)) continue; // Skip used URL
                        if (strpos($slug, $lower_keyword) !== false) { // Relevance check
                            // DOM replacement
                            $text_before = mb_substr($node_text, 0, $position, 'UTF-8'); $text_after = mb_substr($node_text, $position + mb_strlen($original_case_keyword, 'UTF-8'), null, 'UTF-8'); $link = $dom->createElement('a', $original_case_keyword); $link->setAttribute('href', esc_url($url)); $node_before = !empty($text_before) ? $dom->createTextNode($text_before) : null; $node_after = !empty($text_after) ? $dom->createTextNode($text_after) : null; $parent = $node->parentNode; if (!$parent) continue;
                            if ($node_before) $parent->insertBefore($node_before, $node); $parent->insertBefore($link, $node); if ($node_after) $parent->insertBefore($node_after, $node); $parent->removeChild($node);
                            // Update state
                            $links_added++; $used_urls[] = $url; $used_keywords[] = $lower_keyword; $modified = true; $node_modified_in_loop = true; $links_per_paragraph[$p_hash] = $count_in_p + 1;
                            break 2; // Exit loops
                        }
                    } // End sitemap URL loop
                } // End word boundary check
            } // End keyword position check
        } // End keyword loop
    } // End text node loop

    // Update Post
    if ($modified) {
        $new_content = ''; foreach ($dom->childNodes as $child) { $new_content .= $dom->saveHTML($child); } if(empty(trim($new_content))) { $bodyNode = $xpath->query('//body')->item(0); if ($bodyNode) { foreach ($bodyNode->childNodes as $child) { $new_content .= $dom->saveHTML($child); } } }
        if (empty($new_content) || strlen($new_content) < strlen($original_content) / 3) { error_log("Auto Interlinking DOM Error ($post_id): New content invalid after saveHTML (V2.9). Aborting update."); return; }
        remove_action('save_post_post', 'my_auto_interlink_post_dom', 20); wp_update_post(['ID' => $post_id, 'post_content' => $new_content]); add_action('save_post_post', 'my_auto_interlink_post_dom', 20, 2);
        error_log("Auto Interlinking DOM ($post_id): Updated post with {$links_added} links (V2.9).");
    }
}

// ===========================================
// Action Hooks
// ===========================================
add_action('admin_menu', 'my_interlinking_add_admin_menu');
add_action('admin_init', 'my_interlinking_settings_init');
add_action('save_post_post', 'my_auto_interlink_post_dom', 20, 2);

?> // Added closing PHP tag just in case it was expected
Great share, i've used some similar plugins, but this one looks better
 
I mentioned how to do it before, but since I have not much to do atm, I can repeat myself again.

If you set it up correctly, and by that, I mean you:

1. Installed the code snippet plugin
2. Added my snippet
3. Went to Auto interlinking settings (under settings tab at wp admin menu)
4. Added your desired sitemaps there

Then, all you need to do is to go to "Posts" from the WordPress admin menu, select a maximum of 20 posts (old ones) and then do bulk edit in the edit window find "Status" setting, swap from "- No Change -" to published and then save. Do the same thing for all other old posts of yours, hence, you could do more posts per time, but that depends on your server capabilities. In your case, I would do 10-20.
I did everything you tell us but nothing happens..what can be the fix?
 
I appreciate it. I have added the code, now waiting for the process to complete.
 
Back
Top