• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom broken link checker block for Gutenberg using REST API custom routes

Step-by-Step Guide to building a custom broken link checker block for Gutenberg using REST API custom routes

Leveraging WordPress REST API for a Custom Broken Link Checker

Building a robust broken link checker within the WordPress admin area often involves complex database queries and potentially performance-intensive operations. This guide outlines a more scalable and decoupled approach: creating a custom Gutenberg block that interacts with a dedicated REST API endpoint. This strategy separates the link checking logic from the core WordPress rendering, allowing for asynchronous processing and a more responsive user experience.

Defining the REST API Endpoint

We’ll start by registering a custom REST API route to handle the broken link checking requests. This route will accept a POST request, initiate the check, and return the results. For simplicity, this example will perform a synchronous check, but in a production environment, you’d want to queue this task for background processing.

Registering the Route

Add the following code to your plugin’s main file or a dedicated API handler file. Ensure this file is included in your plugin’s activation hook or loaded appropriately.

<?php
/**
 * Plugin Name: Custom Broken Link Checker
 * Description: A custom broken link checker using Gutenberg and REST API.
 * Version: 1.0
 * Author: Your Name
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Register the custom REST API route for broken link checking.
 */
function cblc_register_api_route() {
    register_rest_route( 'cblc/v1', '/check-links', array(
        'methods'  => WP_REST_Server::CREATABLE, // Use CREATABLE for POST requests
        'callback' => 'cblc_check_links_callback',
        'permission_callback' => function() {
            // Ensure only users with 'manage_options' capability can access this.
            return current_user_can( 'manage_options' );
        },
    ) );
}
add_action( 'rest_api_init', 'cblc_register_api_route' );

/**
 * Callback function for the broken link checking API endpoint.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 */
function cblc_check_links_callback( WP_REST_Request $request ) {
    // In a real-world scenario, this would trigger a background job.
    // For this example, we'll perform a synchronous check.
    $links = cblc_get_all_links(); // Placeholder for fetching all links

    if ( empty( $links ) ) {
        return new WP_Error( 'no_links_found', 'No links found to check.', array( 'status' => 404 ) );
    }

    $broken_links = array();
    foreach ( $links as $link_data ) {
        $url = $link_data['url'];
        $response_code = wp_remote_head( $url, array( 'timeout' => 5 ) ); // Use HEAD request for efficiency

        if ( is_wp_error( $response_code ) ) {
            $broken_links[] = array(
                'url' => $url,
                'status' => 'error',
                'message' => $response_code->get_error_message(),
            );
        } elseif ( $response_code['response']['code'] >= 400 ) {
            $broken_links[] = array(
                'url' => $url,
                'status' => 'broken',
                'code' => $response_code['response']['code'],
            );
        }
    }

    return new WP_REST_Response( array(
        'success' => true,
        'message' => 'Link check complete.',
        'broken_links' => $broken_links,
    ), 200 );
}

/**
 * Placeholder function to retrieve all links from the site.
 * In a real implementation, this would scan posts, pages, comments, custom fields, etc.
 * For demonstration, we'll return a hardcoded list.
 *
 * @return array An array of link data.
 */
function cblc_get_all_links() {
    // This is a highly simplified example. A real implementation would involve:
    // - Querying posts, pages, custom post types.
    // - Parsing post content for <a href="..."> tags.
    // - Checking custom fields for URLs.
    // - Potentially checking comment content.
    // - Using a library like `WP_HTML_Tag_Processor` for robust HTML parsing.

    // Example: Fetching links from a custom meta field for demonstration
    $args = array(
        'post_type' => 'post', // Or any other post type
        'posts_per_page' => -1,
        'meta_key' => '_external_links', // Assuming a meta field storing an array of URLs
        'meta_query' => array(
            array(
                'key' => '_external_links',
                'compare' => 'EXISTS',
            ),
        ),
    );
    $posts_with_links = get_posts( $args );

    $all_links = array();
    foreach ( $posts_with_links as $post ) {
        $external_links = get_post_meta( $post->ID, '_external_links', true );
        if ( is_array( $external_links ) ) {
            foreach ( $external_links as $link_url ) {
                if ( filter_var( $link_url, FILTER_VALIDATE_URL ) ) {
                    $all_links[] = array(
                        'url' => esc_url_raw( $link_url ),
                        'source' => 'post_meta',
                        'post_id' => $post->ID,
                    );
                }
            }
        }
    }

    // Add some known good and bad links for testing
    $all_links[] = array('url' => 'https://www.google.com', 'source' => 'test');
    $all_links[] = array('url' => 'https://httpbin.org/status/404', 'source' => 'test');
    $all_links[] = array('url' => 'https://invalid-domain-that-does-not-exist-12345.com', 'source' => 'test');
    $all_links[] = array('url' => 'https://httpbin.org/status/500', 'source' => 'test');

    return $all_links;
}

In cblc_get_all_links(), the current implementation is a placeholder. A production-ready version would need to thoroughly scan your site’s content, including post bodies, custom fields, and potentially comments, to extract all outbound URLs. Libraries like WP_HTML_Tag_Processor can be invaluable for safely parsing HTML content.

Developing the Gutenberg Block

Now, let’s create the Gutenberg block that will serve as our user interface for initiating the link check and displaying results. This involves creating a JavaScript file for the block’s editor and frontend rendering, and a PHP file to register the block type.

Block Registration (PHP)

Register the block type in your plugin’s PHP file. This tells WordPress about your new block and where to find its JavaScript and CSS assets.

<?php
/**
 * Registers the custom block.
 */
function cblc_register_block() {
    // Automatically load dependencies and version.
    $asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php' );

    register_block_type( 'cblc/broken-link-checker', array(
        'api_version' => 2,
        'editor_script' => 'cblc-broken-link-checker-editor-script',
        'editor_style'  => 'cblc-broken-link-checker-editor-style',
        'style'         => 'cblc-broken-link-checker-style',
        'script'        => 'cblc-broken-link-checker-script',
        'dependencies'  => $asset_file['dependencies'],
        'version'       => $asset_file['version'],
    ) );
}
add_action( 'init', 'cblc_register_block' );

/**
 * Enqueue block assets.
 */
function cblc_enqueue_block_assets() {
    // Register scripts and styles.
    wp_register_script(
        'cblc-broken-link-checker-editor-script',
        plugins_url( 'build/index.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    wp_register_style(
        'cblc-broken-link-checker-editor-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array( 'wp-edit-blocks' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
    );

    wp_register_style(
        'cblc-broken-link-checker-style',
        plugins_url( 'build/style-index.css', __FILE__ ),
        array(),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
    );

    // Localize script with API URL
    wp_localize_script( 'cblc-broken-link-checker-editor-script', 'cblc_data', array(
        'rest_url' => esc_url_raw( rest_url( 'cblc/v1/check-links' ) ),
        'nonce'    => wp_create_nonce( 'wp_rest' ),
    ) );
}
add_action( 'enqueue_block_editor_assets', 'cblc_enqueue_block_assets' );

The wp_localize_script call is crucial here. It makes the REST API endpoint URL and a nonce (for authentication) available to our JavaScript code.

Block JavaScript (React)

This is the core of our Gutenberg block. We’ll use React to build the UI. This code would typically reside in a src/index.js file and be compiled using tools like `@wordpress/scripts`.

// src/index.js
const { registerBlockType } = wp.blocks;
const { useState, useEffect } = wp.element;
const { Button, PanelBody, Text, Spinner, Notice } = wp.components;
const { __ } = wp.i18n;

const BrokenLinkCheckerBlock = () => {
    const [ isChecking, setIsChecking ] = useState( false );
    const [ results, setResults ] = useState( null );
    const [ error, setError ] = useState( null );

    const checkLinks = async () => {
        setIsChecking( true );
        setError( null );
        setResults( null );

        try {
            const response = await fetch( cblc_data.rest_url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': cblc_data.nonce,
                },
            } );

            const data = await response.json();

            if ( ! response.ok ) {
                // Handle API errors
                const errorMessage = data.message || __( 'An unknown error occurred.', 'cblc' );
                setError( errorMessage );
                setResults( null );
            } else {
                // Handle successful response
                setResults( data.broken_links || [] );
                if ( data.broken_links && data.broken_links.length === 0 ) {
                    setError( __( 'No broken links found!', 'cblc' ) );
                } else if ( ! data.broken_links ) {
                    setError( __( 'No broken links data received.', 'cblc' ) );
                }
            }
        } catch ( e ) {
            // Handle network errors or JSON parsing errors
            setError( __( 'Network error or failed to connect to the server.', 'cblc' ) );
            setResults( null );
        } finally {
            setIsChecking( false );
        }
    };

    return (
        <PanelBody title={ __( 'Broken Link Checker', 'cblc' ) } initialOpen={ true }>
            <Button isPrimary onClick={ checkLinks } disabled={ isChecking }>
                { isChecking ? <Spinner /> : __( 'Check for Broken Links', 'cblc' ) }
            </Button>

            { error && <Notice status="error" isDismissible={false}>{error}</Notice> }

            { results && results.length > 0 && (
                <div>
                    <h4>{ __( 'Broken Links Found:', 'cblc' ) }</h4>
                    <ul>
                        { results.map( ( link, index ) => (
                            <li key={ index }>
                                <Text tag="a" href={ link.url } target="_blank" rel="noopener noreferrer">{ link.url }</Text>
                                 - 
                                <Text>{ link.status === 'broken' ? `Status: ${ link.code }` : link.message }</Text>
                            </li>
                        ) ) }
                    </ul>
                </div>
            ) }
            { results && results.length === 0 && !error && (
                 <Notice status="success" isDismissible={false}>{ __( 'No broken links found!', 'cblc' ) }</Notice>
            ) }
        </PanelBody>
    );
};

registerBlockType( 'cblc/broken-link-checker', {
    title: __( 'Broken Link Checker', 'cblc' ),
    icon: 'admin-links', // WordPress Dashicon
    category: 'widgets', // Or any other suitable category
    edit: BrokenLinkCheckerBlock,
    save: () => null, // This block is purely for the editor interface
} );

To compile this JavaScript, you’ll need to set up a build process. The easiest way is to use @wordpress/scripts. Install it via npm:

npm install --save-dev @wordpress/scripts

Then, add a script to your package.json:

{
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  }
}

Run npm run build to compile your JavaScript and CSS into the build directory. The wp-scripts build command will automatically generate the index.asset.php file needed for block registration.

Integrating and Using the Block

Once your plugin is activated and the build process is complete, you can add the “Broken Link Checker” block to any post, page, or custom post type within the Gutenberg editor. Clicking the “Check for Broken Links” button will trigger the API request. The results, including any broken links found, will be displayed directly within the block.

Enhancements and Production Considerations

This example provides a foundational structure. For a production-ready solution, consider the following:

  • Asynchronous Processing: The current API callback is synchronous. For large sites, this will time out. Implement a background job queue (e.g., using WP-Cron with a robust queuing system or an external service like Redis Queue) to handle link checking asynchronously. The API endpoint would then trigger the job, and the block could poll for status updates or use WebSockets.
  • Link Extraction Robustness: The cblc_get_all_links() function needs significant enhancement to reliably extract all URLs from various sources (post content, custom fields, theme options, etc.). Use proper HTML parsing and regular expressions carefully.
  • Rate Limiting and Error Handling: Implement rate limiting on your API endpoint to prevent abuse. Improve error handling for various network conditions and server responses.
  • Caching: Cache the results of link checks to avoid redundant requests and improve performance.
  • User Feedback: Provide more granular feedback to the user during the checking process (e.g., “Checking 1 of 100 links…”).
  • Link Management: Consider adding features to mark links as “ignored,” “fixed,” or to directly edit the source of the link.
  • Security: Always sanitize and validate any user input and ensure proper nonce verification for API requests. The permission callback is essential.
  • Internationalization: Ensure all user-facing strings are translatable using WordPress’s i18n functions (e.g., __(), _x()).

By combining the power of the WordPress REST API with the flexibility of Gutenberg blocks, you can create sophisticated, user-friendly tools that extend the functionality of WordPress without cluttering the core experience.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • How to design a modular Action-hook Event Mediator architecture for enterprise-level custom plugins
  • Step-by-Step Guide to building a custom database optimizer portal block for Gutenberg using Next.js headless configurations
  • Optimizing WooCommerce cart response times by lazy loading custom user transaction ledgers assets
  • Step-by-Step Guide: Offloading high-frequency custom subscription logs metadata writes to a Redis KV store
  • How to design a modular Command Query Responsibility Segregation (CQRS) architecture for enterprise-level custom plugins

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (41)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (65)
  • WordPress Plugin Development (72)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • How to design a modular Action-hook Event Mediator architecture for enterprise-level custom plugins
  • Step-by-Step Guide to building a custom database optimizer portal block for Gutenberg using Next.js headless configurations
  • Optimizing WooCommerce cart response times by lazy loading custom user transaction ledgers assets

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala