wishme
Junior Member
- Dec 4, 2014
- 149
- 161
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:


This interlinking strategy follows these guidelines:
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.
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:


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, 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
Last edited: