[Update] Link inline to categories automatically

Hey,

we know how important internal linking is and wanted to automate a bit of the process. For that, we’ve added the following Code to do the following:

  • Caches categories and stores them
  • Links automatically to all name-matching categories in posts
  • Updates the Cache when changes to categories are made
  • Updates the post automatically; no linking necessary

To use this, simply add this code to your functions.php file, or by using a Plugin like Code Snippets.

Newest Version

This code adds a check to only add the link to a category only to the first 2 instances; significally decreasing load time.

class Category_Link_Optimizer {
    private static $instance = null;
    private $category_links = [];
    private $cache_key = 'category_links_cache';
    private $cache_time = 3600; // 1 hour
    private $excluded_words = ['Amazon', 'gooloo'];
    private $max_links_per_category = 2;

    private function __construct() {
        add_filter('the_content', [$this, 'link_categories_in_content'], 99);
        add_action('edited_category', [$this, 'clear_cache']);
        add_action('create_category', [$this, 'clear_cache']);
        add_action('delete_category', [$this, 'clear_cache']);
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function get_cached_category_links() {
        if (!empty($this->category_links)) {
            return $this->category_links;
        }

        $this->category_links = wp_cache_get($this->cache_key);

        if (false === $this->category_links) {
            $this->category_links = $this->generate_category_links();
            wp_cache_set($this->cache_key, $this->category_links, '', $this->cache_time);
        }

        return $this->category_links;
    }

    private function generate_category_links() {
        $categories = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
        $links = [];

        foreach ($categories as $category) {
            if (!in_array($category->name, $this->excluded_words)) {
                $links[$category->name] = get_category_link($category->term_id);
            }
        }

        return $links;
    }

    private function replace_category_with_limit($content, $name, $url) {
        $count = 0;
        $pattern = '/\b' . preg_quote($name, '/') . '\b(?![^<]*>)/i';
        
        return preg_replace_callback($pattern, function($matches) use (&$count, $url, $name) {
            if ($count < $this->max_links_per_category) {
                $count++;
                return '<a href="' . esc_url($url) . '">' . $name . '</a>';
            }
            return $matches[0];
        }, $content);
    }

    public function link_categories_in_content($content) {
        // Check if it's a single post and specifically of post type 'post'
        if (!is_single() || get_post_type() !== 'post') {
            return $content;
        }

        $category_links = $this->get_cached_category_links();

        // Sort category names by length (descending) to match longer names first
        uksort($category_links, function($a, $b) {
            return strlen($b) - strlen($a);
        });

        // Split content by the amazon search div
        $parts = preg_split('/<div class=["\']amazon-search["\'].*?<\/div>/s', $content);
        
        // Process each part separately
        foreach ($parts as &$part) {
            foreach ($category_links as $name => $url) {
                $part = $this->replace_category_with_limit($part, $name, $url);
            }
        }

        // Reassemble the content with the original amazon search divs
        $amazon_search_divs = [];
        preg_match_all('/<div class=["\']amazon-search["\'].*?<\/div>/s', $content, $amazon_search_divs);
        
        $final_content = '';
        for ($i = 0; $i < count($parts); $i++) {
            $final_content .= $parts[$i];
            if (isset($amazon_search_divs[0][$i])) {
                $final_content .= $amazon_search_divs[0][$i];
            }
        }

        return $final_content;
    }

    public function clear_cache() {
        wp_cache_delete($this->cache_key);
        $this->category_links = [];

        // Clear LiteSpeed Cache if it's active
        if (class_exists('LiteSpeed_Cache_API')) {
            LiteSpeed_Cache_API::purge_all();
        }
    }
}

// Initialize the Category Link Optimizer
add_action('init', function() {
    Category_Link_Optimizer::get_instance();
});

New Version

This Version is a little more comprehensive and better with dynamically added content. It also uses less resources. In this example, we’re also excluding specific categories. In this case, “Amazon”.

Note: The Plugin linked below is still the “old version”.

class Category_Link_Optimizer {
    private static $instance = null;
    private $category_links = [];
    private $cache_key = 'category_links_cache';
    private $cache_time = 3600; // 1 hour
    private $excluded_words = ['Amazon', 'gooloo'];

    private function __construct() {
        add_filter('the_content', [$this, 'link_categories_in_content'], 99);
        add_action('edited_category', [$this, 'clear_cache']);
        add_action('create_category', [$this, 'clear_cache']);
        add_action('delete_category', [$this, 'clear_cache']);
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function get_cached_category_links() {
        if (!empty($this->category_links)) {
            return $this->category_links;
        }

        $this->category_links = wp_cache_get($this->cache_key);

        if (false === $this->category_links) {
            $this->category_links = $this->generate_category_links();
            wp_cache_set($this->cache_key, $this->category_links, '', $this->cache_time);
        }

        return $this->category_links;
    }

    private function generate_category_links() {
        $categories = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
        $links = [];

        foreach ($categories as $category) {
            if (!in_array($category->name, $this->excluded_words)) {
                $links[$category->name] = get_category_link($category->term_id);
            }
        }

        return $links;
    }

    public function link_categories_in_content($content) {
        // Check if it's a single post, not a page or other post type
        if (empty($content) || !is_single() || !is_singular('post')) {
            return $content;
        }

        $category_links = $this->get_cached_category_links();

        // Sort category names by length (descending) to match longer names first
        uksort($category_links, function($a, $b) {
            return strlen($b) - strlen($a);
        });

        // Split content by the amazon search div
        $parts = preg_split('/<div class=["\']amazon-search["\'].*?<\/div>/s', $content);
        
        // Process each part separately
        foreach ($parts as &$part) {
            foreach ($category_links as $name => $url) {
                $pattern = '/\b' . preg_quote($name, '/') . '\b(?![^<]*>)/i';
                $replacement = '<a href="' . esc_url($url) . '">' . $name . '</a>';
                $part = preg_replace($pattern, $replacement, $part);
            }
        }

        // Reassemble the content with the original amazon search divs
        $amazon_search_divs = [];
        preg_match_all('/<div class=["\']amazon-search["\'].*?<\/div>/s', $content, $amazon_search_divs);
        
        $final_content = '';
        for ($i = 0; $i < count($parts); $i++) {
            $final_content .= $parts[$i];
            if (isset($amazon_search_divs[0][$i])) {
                $final_content .= $amazon_search_divs[0][$i];
            }
        }

        return $final_content;
    }

    public function clear_cache() {
        wp_cache_delete($this->cache_key);
        $this->category_links = [];

        // Clear LiteSpeed Cache if it's active
        if (class_exists('LiteSpeed_Cache_API')) {
            LiteSpeed_Cache_API::purge_all();
        }
    }
}

// Initialize the Category Link Optimizer
add_action('init', function() {
    Category_Link_Optimizer::get_instance();
});

Previous Version

class Category_Link_Optimizer {
    private static $instance = null;
    private $category_links = [];
    private $cache_key = 'category_links_cache';
    private $cache_time = 3600; // 1 hour

    private function __construct() {
        add_filter('the_content', [$this, 'link_categories_in_content'], 20);
        add_action('edited_category', [$this, 'clear_cache']);
        add_action('create_category', [$this, 'clear_cache']);
        add_action('delete_category', [$this, 'clear_cache']);
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function get_cached_category_links() {
        if (!empty($this->category_links)) {
            return $this->category_links;
        }

        $this->category_links = wp_cache_get($this->cache_key);

        if (false === $this->category_links) {
            $this->category_links = $this->generate_category_links();
            wp_cache_set($this->cache_key, $this->category_links, '', $this->cache_time);
        }

        return $this->category_links;
    }

    private function generate_category_links() {
        $categories = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
        $links = [];

        foreach ($categories as $category) {
            $links[$category->name] = get_category_link($category->term_id);
        }

        return $links;
    }

    public function link_categories_in_content($content) {
        if (empty($content) || !is_singular('post')) {
            return $content;
        }

        $category_links = $this->get_cached_category_links();

        // Sort category names by length (descending) to match longer names first
        uksort($category_links, function($a, $b) {
            return strlen($b) - strlen($a);
        });

        $batch_size = 100;
        $total_categories = count($category_links);

        for ($i = 0; $i < $total_categories; $i += $batch_size) {
            $batch = array_slice($category_links, $i, $batch_size, true);

            foreach ($batch as $name => $url) {
                $pattern = '/\b' . preg_quote($name, '/') . '\b(?![^<]*>)/i';
                $replacement = '<a href="' . esc_url($url) . '">' . $name . '</a>';
                $content = preg_replace($pattern, $replacement, $content);
            }
        }

        return $content;
    }

    public function clear_cache() {
        wp_cache_delete($this->cache_key);
        $this->category_links = [];

        // Clear LiteSpeed Cache if it's active
        if (class_exists('LiteSpeed_Cache_API')) {
            LiteSpeed_Cache_API::purge_all();
        }
    }
}

// Initialize the Category Link Optimizer
add_action('init', function() {
    Category_Link_Optimizer::get_instance();
});

You can also create a Plugin for this. Add these prefixes to the code or download it here directly.

<?php
/*
 * Plugin Name: Category Link Optimizer by gooloo.de
 * Description: This code links category names in post content to their respective category pages.
 * Version: 1.0.0
 * Requires at least: 6.0
 * Requires PHP: 8.0
 * Author: gooloo.de
 */

class Category_Link_Optimizer {
    private static $instance = null;
    private $category_links = [];
    private $cache_key = 'category_links_cache';
    private $cache_time = 3600; // 1 hour

    private function __construct() {
        add_filter('the_content', [$this, 'link_categories_in_content'], 20);
        add_action('edited_category', [$this, 'clear_cache']);
        add_action('create_category', [$this, 'clear_cache']);
        add_action('delete_category', [$this, 'clear_cache']);
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function get_cached_category_links() {
        if (!empty($this->category_links)) {
            return $this->category_links;
        }

        $this->category_links = wp_cache_get($this->cache_key);

        if (false === $this->category_links) {
            $this->category_links = $this->generate_category_links();
            wp_cache_set($this->cache_key, $this->category_links, '', $this->cache_time);
        }

        return $this->category_links;
    }

    private function generate_category_links() {
        $categories = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
        $links = [];

        foreach ($categories as $category) {
            $links[$category->name] = get_category_link($category->term_id);
        }

        return $links;
    }

    public function link_categories_in_content($content) {
        if (empty($content) || !is_singular('post')) {
            return $content;
        }

        $category_links = $this->get_cached_category_links();

        // Sort category names by length (descending) to match longer names first
        uksort($category_links, function($a, $b) {
            return strlen($b) - strlen($a);
        });

        $batch_size = 100;
        $total_categories = count($category_links);

        for ($i = 0; $i < $total_categories; $i += $batch_size) {
            $batch = array_slice($category_links, $i, $batch_size, true);

            foreach ($batch as $name => $url) {
                $pattern = '/\b' . preg_quote($name, '/') . '\b(?![^<]*>)/i';
                $replacement = '<a href="' . esc_url($url) . '">' . $name . '</a>';
                $content = preg_replace($pattern, $replacement, $content);
            }
        }

        return $content;
    }

    public function clear_cache() {
        wp_cache_delete($this->cache_key);
        $this->category_links = [];

        // Clear LiteSpeed Cache if it's active
        if (class_exists('LiteSpeed_Cache_API')) {
            LiteSpeed_Cache_API::purge_all();
        }
    }
}

// Initialize the Category Link Optimizer
add_action('init', function() {
    Category_Link_Optimizer::get_instance();
});

Update 1

This update makes sure that these changes only apply to single posts, not pages, or other post types, and also excludes text within <div> containers.

It also excludes specific shortcodes and words, that can be changed in their respective code field, f.ex. “aawp”, “amazon”, and the DIV Class “.contentbox”.

class Category_Link_Optimizer {
    private static $instance = null;
    private $category_links = [];
    private $cache_key = 'category_links_cache';
    private $cache_time = 3600; // 1 hour
    private $excluded_containers = [
        'div.contentbox',
        'div.aawp',  // AAWP container
        'div[class^="aawp-"]', // Classes starting with aawp-
        'div.wp-block-embed', // Embedded content
        'div.plugin-content', // Generic class for plugin content
    ];
    private $excluded_shortcodes = ['aawp', 'product', 'amazon'];
    private $excluded_words = ['Amazon'];

    private function __construct() {
        add_filter('the_content', [$this, 'link_categories_in_content'], 20);
        add_action('edited_category', [$this, 'clear_cache']);
        add_action('create_category', [$this, 'clear_cache']);
        add_action('delete_category', [$this, 'clear_cache']);
    }

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function get_cached_category_links() {
        if (!empty($this->category_links)) {
            return $this->category_links;
        }

        $this->category_links = wp_cache_get($this->cache_key);

        if (false === $this->category_links) {
            $this->category_links = $this->generate_category_links();
            wp_cache_set($this->cache_key, $this->category_links, '', $this->cache_time);
        }

        return $this->category_links;
    }

    private function generate_category_links() {
        $categories = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
        $links = [];

        foreach ($categories as $category) {
            if (!in_array($category->name, $this->excluded_words)) {
                $links[$category->name] = get_category_link($category->term_id);
            }
        }

        return $links;
    }

    public function link_categories_in_content($content) {
        // Check if it's a single post, not a page or other post type
        if (empty($content) || !is_single() || !is_singular('post')) {
            return $content;
        }

        // Remove shortcodes before processing
        $content = $this->remove_excluded_shortcodes($content);

        $category_links = $this->get_cached_category_links();

        // Sort category names by length (descending) to match longer names first
        uksort($category_links, function($a, $b) {
            return strlen($b) - strlen($a);
        });

        // Split content into excludable and non-excludable parts
        $parts = $this->split_content($content);

        foreach ($parts as &$part) {
            if ($part['exclude']) {
                continue; // Skip excluded parts
            }

            $batch_size = 100;
            $total_categories = count($category_links);

            for ($i = 0; $i < $total_categories; $i += $batch_size) {
                $batch = array_slice($category_links, $i, $batch_size, true);

                foreach ($batch as $name => $url) {
                    $pattern = '/\b' . preg_quote($name, '/') . '\b(?![^<]*>)/i';
                    $replacement = '<a href="' . esc_url($url) . '">' . $name . '</a>';
                    $part['content'] = preg_replace($pattern, $replacement, $part['content']);
                }
            }
        }

        // Reassemble the content
        return $this->reassemble_content($parts);
    }

    private function split_content($content) {
        $parts = [];
        $excluded_regex = $this->get_excluded_regex();

        if (preg_match_all($excluded_regex, $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
            $last_end = 0;
            foreach ($matches as $match) {
                if ($match[0][1] > $last_end) {
                    $parts[] = [
                        'content' => substr($content, $last_end, $match[0][1] - $last_end),
                        'exclude' => false
                    ];
                }
                $parts[] = [
                    'content' => $match[0][0],
                    'exclude' => true
                ];
                $last_end = $match[0][1] + strlen($match[0][0]);
            }
            if ($last_end < strlen($content)) {
                $parts[] = [
                    'content' => substr($content, $last_end),
                    'exclude' => false
                ];
            }
        } else {
            $parts[] = [
                'content' => $content,
                'exclude' => false
            ];
        }

        return $parts;
    }

    private function reassemble_content($parts) {
        return implode('', array_column($parts, 'content'));
    }

    private function get_excluded_regex() {
        $selectors = array_map('preg_quote', $this->excluded_containers);
        return '/<(' . implode('|', $selectors) . ')[^>]*>.*?<\/\1>/s';
    }

    private function remove_excluded_shortcodes($content) {
        $shortcodes_regex = '\[(' . implode('|', $this->excluded_shortcodes) . ').*?\].*?\[\/\1\]';
        return preg_replace('/' . $shortcodes_regex . '/s', '', $content);
    }

    public function clear_cache() {
        wp_cache_delete($this->cache_key);
        $this->category_links = [];

        // Clear LiteSpeed Cache if it's active
        if (class_exists('LiteSpeed_Cache_API')) {
            LiteSpeed_Cache_API::purge_all();
        }
    }
}

// Initialize the Category Link Optimizer
add_action('init', function() {
    Category_Link_Optimizer::get_instance();
});

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *