DEV Community

Cover image for Building an AI Internal Linking Plugin for WordPress
Harshit Kumar
Harshit Kumar

Posted on

Building an AI Internal Linking Plugin for WordPress

๐ŸŽฏ The Problem That Started It All

As a WordPress developer and AI SEO Specialist, I noticed a recurring pain point across numerous websites: internal linking is crucial for SEO, but it's incredibly tedious to maintain manually.

Imagine managing a blog with 1,000+ articles. Every time you publish new content, you should ideally:

  • Find relevant older posts to link to
  • Update older posts to link to the new content
  • Maintain consistent anchor text
  • Avoid over-optimization
  • Track which pages link where

Doing this manually? Nearly impossible at scale.

Third-party tools exist, but they either:

  • Are resource-heavy and crash shared hosting
  • Lack granular control
  • Have security concerns with external dependencies
  • Cost prohibitive amounts monthly

So I decided: Let's build something better.


๐Ÿ—๏ธ Architecture & Technology Stack

Core Technologies

WordPress Version: 5.0+
PHP Version: 7.4+ (8.0+ recommended)
MySQL: 5.6+
JavaScript: Vanilla JS + jQuery (for admin)
CSS: Custom with responsive design
Enter fullscreen mode Exit fullscreen mode

Why These Choices?

PHP 7.4+: Modern PHP features like typed properties and arrow functions made the code cleaner and more maintainable. Plus, better performance and security.

Vanilla JavaScript: For a plugin that needs to work across diverse WordPress installations, minimizing dependencies was crucial. No React, no Vueโ€”just clean, efficient JavaScript.

Custom Database Tables: Instead of using WordPress post meta (which can become unwieldy), I created dedicated tables for:

  • Keywords management
  • Link tracking
  • Processing queue
  • Trash/recovery system

๐ŸŽจ Plugin Architecture: The Four Pillars

I structured the plugin around four core classes:

1. Database Manager (class-database-manager.php)

class AIINLITO_Database_Manager {
    private $keywords_table;
    private $tracking_table;
    private $queue_table;
    private $trash_table;

    public function create_tables() {
        // Creates optimized tables with proper indexes
    }

    public function add_keyword($keyword, $target_url) {
        // Validates, sanitizes, and stores keywords
    }

    // ... more methods
}
Enter fullscreen mode Exit fullscreen mode

Key Decisions:

  • Dedicated tables over post meta for performance
  • Proper indexing on frequently queried columns
  • Foreign key relationships for data integrity
  • Prepared statements everywhere for security

2. Settings Manager (class-settings-manager.php)

This handles all plugin configurations:

  • Maximum keywords allowed
  • Link density controls (links per 100 words)
  • Post type selection
  • Heading tag controls (H1-H6)
  • Batch processing sizes

The Challenge: Making settings flexible enough for power users but simple enough for beginners.

Solution: Smart defaults with progressive disclosure. Basic settings upfront, advanced options hidden behind toggles.

3. Linking Engine (class-linking-engine.php)

The heart of the plugin. This is where the magic happens:

class AIINLITO_Linking_Engine {

    public function process_keyword_in_content($content, $keyword_data, $post) {
        // 1. Check if keyword already has links
        // 2. Calculate word count and max allowed links
        // 3. Find keyword matches (case-insensitive search)
        // 4. Preserve original text case
        // 5. Insert link with tracking attributes
        // 6. Update database
    }

    // ... more methods
}
Enter fullscreen mode Exit fullscreen mode

Key Features Implemented:

  1. Case-Preserving Matching: If the keyword is \"WordPress SEO\" but the text says \"wordpress seo\", it links the lowercase version without changing it.

  2. Smart Link Placement:

    • Only one link per keyword per page
    • Respects user-defined link density
    • Avoids linking inside existing links
    • Can skip heading tags if configured
  3. WordPress Block Editor Compatible: The engine parses Gutenberg block comments correctly, ensuring links aren't placed in the wrong locations.

4. Admin Manager (class-admin-manager.php)

The user interface layer:

  • AJAX handlers for real-time updates
  • CSV import/export functionality
  • Analytics dashboard
  • Settings interface
  • Trash management

โšก Performance: The Biggest Challenge

Problem: Server Overload

Early prototypes had a major issue: processing thousands of posts would timeout or crash the server.

Solution 1: Background Processing with WordPress Cron

// Schedule recurring processing
wp_schedule_event(time(), 'hourly', 'aiinlito_process_keywords_cron');

// Process in batches
public function process_keywords_batch() {
    $batch_size = $this->settings_manager->get_setting('batch_size', 10);
    $queue_items = $this->db_manager->get_processing_batch($batch_size);

    foreach ($queue_items as $item) {
        // Process one item at a time
        $this->process_single_item($item);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Insight: Don't try to process everything at once. Use a queue system and process items in small, manageable batches.

Solution 2: Smart Caching

Instead of hitting the database repeatedly:

  • Cache keyword lists in memory during processing
  • Use WordPress transients for frequently accessed data
  • Implement object caching support

Solution 3: Processing Queue

CREATE TABLE processing_queue (
    id int(11) NOT NULL AUTO_INCREMENT,
    post_id int(11) NOT NULL,
    keyword_id int(11) DEFAULT NULL,
    status varchar(20) DEFAULT 'pending',
    priority int(11) DEFAULT 0,
    created_date datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY post_id (post_id),
    KEY status (status),
    KEY priority (priority)
)
Enter fullscreen mode Exit fullscreen mode

This queue system allows:

  • Prioritization: New posts get processed first
  • Fault tolerance: Failed items can be retried
  • Progress tracking: Users see real-time updates
  • Scalability: Can process millions of posts over time

๐Ÿ”’ Security: No Compromises

WordPress plugin security is critical. Here's what I implemented:

1. Nonce Verification on Every AJAX Call

public function ajax_add_keyword() {
    // Verify nonce
    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'aiinlito_admin_nonce')) {
        wp_die('Security check failed');
    }

    // Check capabilities
    if (!current_user_can('manage_options')) {
        wp_die('Insufficient permissions');
    }

    // Process request...
}
Enter fullscreen mode Exit fullscreen mode

2. Input Sanitization & Validation

Every single input goes through:

  • sanitize_text_field() for text
  • esc_url_raw() for URLs
  • intval() for integers
  • esc_sql() for database queries

3. Prepared Statements

// NEVER do this:
$wpdb->query(\"SELECT * FROM table WHERE id = $id\"); // โŒ SQL Injection risk

// ALWAYS do this:
$wpdb->prepare(\"SELECT * FROM table WHERE id = %d\", $id); // โœ… Safe
Enter fullscreen mode Exit fullscreen mode

4. CSRF Protection

All form submissions require valid nonces and capability checks.

5. External URL Blocking

private function is_external_url($url) {
    $site_url = get_site_url();
    $parsed_site = wp_parse_url($site_url);
    $parsed_url = wp_parse_url($url);

    return isset($parsed_url['host']) && 
           $parsed_url['host'] !== $parsed_site['host'];
}
Enter fullscreen mode Exit fullscreen mode

This prevents users from accidentally (or maliciously) adding external links.


๐ŸŽฏ Advanced Features Implementation

1. Smart Trash System (30-Day Recovery)

Instead of permanent deletion, keywords go to a trash table:

public function delete_keyword($keyword_id) {
    // 1. Get keyword data
    $keyword_data = $this->get_keyword($keyword_id);

    // 2. Move to trash (expires in 30 days)
    $expire_date = date('Y-m-d H:i:s', strtotime('+30 days'));
    $this->wpdb->insert($this->trash_table, [
        'original_keyword_id' => $keyword_data->id,
        'keyword' => $keyword_data->keyword,
        'target_url' => $keyword_data->target_url,
        'expires_date' => $expire_date
    ]);

    // 3. Remove all links from content
    $this->remove_keyword_links($keyword_id);

    // 4. Delete from active keywords
    $this->wpdb->delete($this->keywords_table, ['id' => $keyword_id]);
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters: Users can safely delete keywords knowing they can restore them if needed. The automatic expiration keeps the database clean.

2. Bulk Import with CSV

private function import_from_csv($file_path) {
    $imported = 0;
    $duplicates = 0;

    // Read CSV file using WordPress filesystem API
    global $wp_filesystem;
    WP_Filesystem();
    $file_contents = $wp_filesystem->get_contents($file_path);
    $lines = explode(\"
\", $file_contents);

    foreach ($lines as $line) {
        $data = str_getcsv($line);
        $keyword = trim($data[0]);
        $target_url = trim($data[1]);

        // Validate and add
        $result = $this->db_manager->add_keyword($keyword, $target_url);
        if ($result['success']) {
            $imported++;
        }
    }

    return [
        'imported' => $imported,
        'duplicates' => $duplicates
    ];
}
Enter fullscreen mode Exit fullscreen mode

User Experience: Instead of manually adding 500 keywords one by one, users can import them all in seconds.

3. Real-Time Progress Tracking

Using AJAX polling:

function checkProgress() {
    $.ajax({
        url: aiinlito_ajax.ajax_url,
        type: 'POST',
        data: {
            action: 'aiinlito_get_progress',
            nonce: aiinlito_ajax.progress_nonce
        },
        success: function(response) {
            if (response.success) {
                updateProgressBar(response.data.percentage);

                if (response.data.pending > 0) {
                    // Still processing, check again in 2 seconds
                    setTimeout(checkProgress, 2000);
                }
            }
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Result: Users see a smooth progress bar instead of a frozen screen.

4. Keyword Usage Analytics

public function get_keyword_usage_data($keyword_id) {
    global $wpdb;

    // Get linked pages
    $linked_pages = $wpdb->get_results($wpdb->prepare(
        \"SELECT DISTINCT post_id, post_type, created_date 
         FROM {$this->tracking_table} 
         WHERE keyword_id = %d 
         ORDER BY created_date DESC\",
        $keyword_id
    ));

    // Get post details
    $pages_data = [];
    foreach ($linked_pages as $page) {
        $post = get_post($page->post_id);
        if ($post) {
            $pages_data[] = [
                'title' => $post->post_title,
                'url' => get_permalink($post->ID),
                'linked_date' => $page->created_date
            ];
        }
    }

    return $pages_data;
}
Enter fullscreen mode Exit fullscreen mode

Value: Users can see exactly where each keyword is used, helping with SEO audits.


๐ŸŽจ UI/UX Design Philosophy

Principles I Followed:

  1. Progressive Disclosure: Don't overwhelm users. Show basic options first, advanced settings behind a toggle.

  2. Immediate Feedback: Every action gets instant visual confirmation.

  3. Mobile-First: The admin interface works perfectly on phones and tablets.

  4. Accessibility: Proper ARIA labels, keyboard navigation, screen reader support.

The Dashboard

// Keywords Page
<div class=\"aiinlito-dashboard\">
    <div class=\"stats-overview\">
        <div class=\"stat-card\">
            <span class=\"stat-number\"><?php echo $total_keywords; ?></span>
            <span class=\"stat-label\">Active Keywords</span>
        </div>
        <div class=\"stat-card\">
            <span class=\"stat-number\"><?php echo $usage_count; ?></span>
            <span class=\"stat-label\">Links Created</span>
        </div>
    </div>

    <div class=\"keywords-table\">
        <!-- Interactive table with sort, search, pagination -->
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Design Goals:

  • Clean, modern interface
  • Color-coded status indicators
  • Smooth animations
  • Responsive grid layout

๐Ÿงช Testing & Quality Assurance

Testing Strategy

  1. Manual Testing:

    • Tested on WordPress 5.0 through 6.8.2
    • Multiple PHP versions (7.4, 8.0, 8.1, 8.2)
    • Various hosting environments (shared, VPS, dedicated)
    • Different themes and page builders
  2. Performance Testing:

    • Tested with databases containing 10,000+ posts
    • Monitored memory usage and query times
    • Optimized slow queries with indexes
  3. Security Audits:

    • Ran through WordPress Plugin Checker
    • Used PHP CodeSniffer with WordPress standards
    • Manual code review for vulnerabilities
  4. Compatibility Testing:

    • Page builders: Elementor, Gutenberg, Classic Editor
    • SEO plugins: Yoast, RankMath, All in One SEO
    • Caching plugins: WP Super Cache, W3 Total Cache
    • Multilingual plugins: WPML, Polylang

๐Ÿ“Š Performance Optimization Techniques

1. Query Optimization

Before:

// Slow: N+1 query problem
foreach ($keywords as $keyword) {
    $usage = $wpdb->get_var(\"SELECT COUNT(*) FROM tracking WHERE keyword_id = {$keyword->id}\");
}
Enter fullscreen mode Exit fullscreen mode

After:

// Fast: Single query with JOIN
$keywords_with_usage = $wpdb->get_results(
    \"SELECT k.*, COUNT(t.id) as usage_count 
     FROM keywords k 
     LEFT JOIN tracking t ON k.id = t.keyword_id 
     GROUP BY k.id\"
);
Enter fullscreen mode Exit fullscreen mode

2. Memory Management

// Process large datasets in chunks
$offset = 0;
$limit = 100;

while ($posts = get_posts([
    'posts_per_page' => $limit,
    'offset' => $offset,
    'post_status' => 'publish'
])) {
    foreach ($posts as $post) {
        process_post($post);
    }
    $offset += $limit;

    // Free memory
    wp_cache_flush();
}
Enter fullscreen mode Exit fullscreen mode

3. Database Indexing

-- Speed up keyword searches
CREATE INDEX idx_keyword_status ON keywords(status);
CREATE INDEX idx_keyword_usage ON keywords(usage_count);

-- Speed up tracking queries
CREATE INDEX idx_tracking_keyword ON link_tracking(keyword_id);
CREATE INDEX idx_tracking_post ON link_tracking(post_id);

-- Speed up queue processing
CREATE INDEX idx_queue_status ON processing_queue(status, priority, created_date);
Enter fullscreen mode Exit fullscreen mode

๐Ÿšง Challenges & Solutions

Challenge 1: WordPress Block Editor (Gutenberg)

Problem: Gutenberg uses HTML comments for blocks:

<!-- wp:paragraph -->
<p>This is content</p>
<!-- /wp:paragraph -->
Enter fullscreen mode Exit fullscreen mode

If we're not careful, links could be inserted inside these comments, breaking the editor.

Solution:

// Remove block comments before searching
$search_content = preg_replace('/<!--\s*wp:.*?-->/s', '', $content);
$search_content = preg_replace('/<!--\s*\/wp:.*?-->/s', '', $search_content);

// Find matches in clean content
$matches = $this->find_keyword_matches($search_content, $keyword);

// Map positions back to original content
$original_position = $this->find_original_position($content, $matched_text);
Enter fullscreen mode Exit fullscreen mode

Challenge 2: Case-Preserving Links

Problem: Users want to add \"WordPress SEO\" as a keyword, but their content might have \"wordpress seo\", \"WORDPRESS SEO\", or \"WordPress seo\". We need to link all variations but preserve their original case.

Solution:

// Case-insensitive search
$pattern = '/\b' . preg_quote($keyword, '/') . '\b/i';

if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) {
    // $match[0][0] contains the actual text with original case
    $matched_text = $match[0][0]; // \"wordpress seo\"
    $position = $match[0][1];

    // Create link preserving the case
    $link = '<a href=\"' . $url . '\">' . $matched_text . '</a>';
}
Enter fullscreen mode Exit fullscreen mode

Challenge 3: Link Removal on Keyword Deletion

Problem: When a keyword is deleted, we need to remove all its links from potentially thousands of posts without breaking the content.

Solution:

private function remove_keyword_links($keyword_id) {
    // Get all posts with this keyword
    $tracking_records = $wpdb->get_results(
        \"SELECT * FROM {$this->tracking_table} WHERE keyword_id = {$keyword_id}\"
    );

    foreach ($tracking_records as $record) {
        $post = get_post($record->post_id);
        $content = $post->post_content;

        // Remove link but keep the text
        $pattern = '/<a[^>]*data-aiinlito-keyword-id=\"' . $keyword_id . '\"[^>]*>(.*?)<\/a>/i';
        $cleaned_content = preg_replace($pattern, '$1', $content);

        // Update post
        wp_update_post([
            'ID' => $post->ID,
            'post_content' => $cleaned_content
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Challenge 4: Shared Hosting Limitations

Problem: Many users are on cheap shared hosting with:

  • Limited PHP memory (64-128MB)
  • Strict execution time limits (30 seconds)
  • Restricted database resources

Solution:

  • Background processing with small batches
  • Configurable batch sizes (default: 10 items)
  • Automatic timeout detection and recovery
  • Memory-efficient algorithms

๐Ÿ“ˆ Lessons Learned

1. Start with Performance in Mind

Don't wait until you have performance issues to optimize. Build efficient systems from day one:

  • Use proper database indexes
  • Implement caching early
  • Batch operations by default
  • Monitor memory usage

2. Security Can't Be an Afterthought

Every AJAX endpoint, every database query, every file upload needs security checks. It's tedious, but one vulnerability can destroy trust.

3. User Experience > Feature Count

I initially wanted to add 50+ features. Instead, I focused on 10 core features done exceptionally well with a beautiful UI. Result? Higher user satisfaction.

4. Testing on Real Sites is Crucial

Local development can't replicate:

  • Slow shared hosting
  • Conflicting plugins
  • Unusual theme configurations
  • Large databases (10,000+ posts)

Always test on real, production-like environments.

5. Documentation Matters

I spent almost as much time on documentation as coding:

  • Detailed README
  • Video tutorials
  • FAQ section
  • Code comments

Result? Far fewer support requests.

6. WordPress Standards Exist for a Reason

Following WordPress Coding Standards:

  • Makes code more maintainable
  • Ensures compatibility with other plugins
  • Provides security best practices
  • Helps with WordPress.org approval

๐Ÿ”ฎ Future Enhancements

Here's what's on the roadmap:

  1. AI Keyword Suggestions: Analyze content and suggest relevant internal linking opportunities

  2. Link Decay Detection: Alert when target URLs return 404s

  3. A/B Testing: Test different anchor texts for the same target URL

  4. Anchor Text Variations: Instead of always using the same keyword, use synonyms

  5. Content Cluster Visualization: Visual map showing how content is interconnected

  6. REST API: Allow external tools to manage keywords programmatically

  7. Link Priority System: Prioritize certain keywords over others

  8. Seasonal Linking: Automatically adjust links based on time of year


๐Ÿ’ก Key Takeaways for WordPress Developers

If you're building a WordPress plugin, here's my advice:

1. Architecture First

โœ… Separate concerns (database, logic, UI)
โœ… Use dependency injection
โœ… Follow SOLID principles
โœ… Make code testable
Enter fullscreen mode Exit fullscreen mode

2. Performance Matters

โœ… Use background processing for heavy tasks
โœ… Implement proper caching
โœ… Optimize database queries
โœ… Test with large datasets
Enter fullscreen mode Exit fullscreen mode

3. Security is Non-Negotiable

โœ… Nonce verification on all AJAX calls
โœ… Capability checks everywhere
โœ… Sanitize inputs, escape outputs
โœ… Use prepared statements
Enter fullscreen mode Exit fullscreen mode

4. User Experience Wins

โœ… Real-time feedback
โœ… Clear error messages
โœ… Intuitive navigation
โœ… Mobile responsiveness
Enter fullscreen mode Exit fullscreen mode

5. Code Quality Matters

โœ… Follow WordPress Coding Standards
โœ… Write meaningful comments
โœ… Use consistent naming conventions
โœ… Keep functions small and focused
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฌ Conclusion

Building AI Internal Linking Tool was a journey that taught me invaluable lessons about:

  • WordPress plugin architecture
  • Performance optimization at scale
  • Security best practices
  • User experience design
  • Code maintainability

The plugin now successfully automates internal linking for thousands of websites, from small blogs to enterprise sites with millions of pages.

Most importantly: It solves a real problem in a way that respects user's servers, follows WordPress standards, and provides genuine value.


๐Ÿ“š Resources & References

WordPress Development:

Performance Optimization:

Security:


๐Ÿ”— Connect & Learn More


Have questions about WordPress plugin development? Drop them in the comments below! ๐Ÿ‘‡

I'm happy to discuss architecture decisions, performance optimization, or any other aspect of the development process.


About the Author ๐Ÿ‘จ

Kumar Harshit

Kumar Harshit - AI SEO Specialist & Tool Developer

Iโ€™m Kumar Harshit, an AI-driven SEO Specialist with over 7 years of hands-on experience in building WordPress solutions that blend speed, scalability, and intelligence. My core mission is to create smart, automation-ready SEO tools that empower websites to rank faster and perform better.

๐ŸŽฏ My Expertise

  • WordPress Development - Custom plugins and performance optimization
  • SEO Optimization - Technical SEO and search engine compliance
  • AI Integration - Implementing AI-powered solutions for web
  • Performance Engineering - Scalable, high-traffic solutions

๐Ÿ› ๏ธ Tools I've Created

"I believe the future of SEO is automation โ€” where smart tools do the heavy lifting, and humans focus on creativity and strategy."


Wrap Up ๐ŸŽฏ

Enjoyed this post? Drop a โค๏ธ and share your take in the comments!
Letโ€™s talk about AI-driven SEO, WordPress performance, or how to build smarter tools for the open web.

Tags: #WordPress #SEO #Performance #PluginDevelopment #GoogleNews #WebDev #PHP #Optimization

Top comments (0)