• 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 Alpine.js lightweight states

Step-by-Step Guide to building a custom broken link checker block for Gutenberg using Alpine.js lightweight states

Leveraging Alpine.js for a Custom Gutenberg Broken Link Checker

For e-commerce platforms built on WordPress, maintaining link integrity is paramount. Broken links not only degrade user experience but also negatively impact SEO. While numerous plugins exist, building a custom Gutenberg block offers granular control and a streamlined workflow. This guide details the construction of a lightweight, client-side broken link checker block using Alpine.js for state management, directly within the WordPress editor.

Prerequisites and Project Setup

Before we begin, ensure you have a local WordPress development environment set up. We’ll be creating a custom plugin to house our Gutenberg block. This approach keeps the functionality separate from your theme, making it portable and maintainable.

Create a new directory in your WordPress installation’s wp-content/plugins/ folder, for example, custom-link-checker. Inside this directory, create a main plugin file, custom-link-checker.php.

Plugin Header

Add the standard WordPress plugin header to custom-link-checker.php:

<?php
/**
 * Plugin Name: Custom Broken Link Checker
 * Description: A custom Gutenberg block to check for broken links within a post or page.
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yourwebsite.com
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain: custom-link-checker
 */

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

Gutenberg Block Registration

We need to register our Gutenberg block. This involves enqueueing JavaScript and CSS files that define the block’s behavior and appearance. We’ll use the WordPress Script and Style Enqueuing API.

Enqueueing Block Assets

In custom-link-checker.php, add the following code to register and enqueue the block’s assets:

/**
 * Register and enqueue block assets.
 */
function clc_register_block() {
	// Register block script.
	wp_register_script(
		'clc-block-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' )
	);

	// Register block style.
	wp_register_style(
		'clc-block-style',
		plugins_url( 'build/style-index.css', __FILE__ ),
		array( 'wp-edit-blocks' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
	);

	// Register block editor style.
	wp_register_style(
		'clc-block-editor-style',
		plugins_url( 'build/index.css', __FILE__ ),
		array( 'clc-block-style' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
	);

	// Register the block.
	register_block_type( 'custom-link-checker/broken-link-checker', array(
		'editor_script' => 'clc-block-editor-script',
		'editor_style'  => 'clc-block-editor-style',
		'style'         => 'clc-block-style',
	) );
}
add_action( 'init', 'clc_register_block' );

Frontend and Editor JavaScript

The core of our block will be in JavaScript. We’ll use the modern `@wordpress/scripts` package to compile our JavaScript and CSS. First, set up your project for compilation. In the root of your plugin directory (custom-link-checker/), create a package.json file:

{
  "name": "custom-link-checker",
  "version": "1.0.0",
  "description": "Custom Gutenberg block for broken link checking.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": [
    "wordpress",
    "gutenberg",
    "block"
  ],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.0.0"
  }
}

After creating package.json, run npm install in your terminal within the plugin directory to install the necessary development dependencies.

Block Definition (src/index.js)

Create a src directory in your plugin’s root. Inside src, create index.js. This file will define the block’s attributes, edit function, and save function.

/**
 * WordPress dependencies
 */
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { useState, useEffect } from '@wordpress/element';
import { Button, Spinner, TextControl, PanelBody, ToggleControl } from '@wordpress/components';

/**
 * Internal dependencies
 */
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles

const blockName = 'custom-link-checker/broken-link-checker';

registerBlockType( blockName, {
	title: __( 'Broken Link Checker', 'custom-link-checker' ),
	icon: 'admin-links',
	category: 'widgets',
	attributes: {
		scanOnLoad: {
			type: 'boolean',
			default: true,
		},
		scanInterval: {
			type: 'number',
			default: 30000, // 30 seconds
		},
	},
	edit: ( { attributes, setAttributes } ) => {
		const blockProps = useBlockProps();
		const { scanOnLoad, scanInterval } = attributes;

		// Alpine.js integration for state management
		const [ links, setLinks ] = useState( [] );
		const [ checking, setChecking ] = useState( false );
		const [ error, setError ] = useState( null );
		const [ progress, setProgress ] = useState( 0 );
		const [ totalLinks, setTotalLinks ] = useState( 0 );

		const handleScan = async () => {
			setChecking( true );
			setError( null );
			setLinks( [] );
			setProgress( 0 );
			setTotalLinks( 0 );

			try {
				// In a real-world scenario, this would involve a server-side AJAX request
				// to fetch all links from the post content and then perform checks.
				// For this example, we'll simulate fetching and checking.

				// Simulate fetching links from the editor content.
				// This is a simplified representation. A real implementation would parse
				// the DOM to extract 'a' tags and their 'href' attributes.
				const editorContent = document.querySelector( '.block-editor-block-list__block[data-block="' + blockProps['data-block'] + '"]' );
				const anchorTags = editorContent ? editorContent.querySelectorAll( 'a[href]' ) : [];
				const extractedLinks = Array.from( anchorTags ).map( a => a.href );

				setTotalLinks( extractedLinks.length );

				if ( extractedLinks.length === 0 ) {
					setChecking( false );
					return;
				}

				const results = [];
				for ( const url of extractedLinks ) {
					// Simulate checking each link. In a production environment,
					// this would be a HEAD request to the URL.
					// Due to browser CORS policies, direct AJAX requests to external URLs
					// from the browser are restricted. A server-side endpoint is required.
					// For demonstration, we'll simulate success/failure.
					const isBroken = Math.random() > 0.8; // 20% chance of being broken
					results.push( { url, status: isBroken ? 'broken' : 'ok', statusCode: isBroken ? 404 : 200 } );
					setProgress( ( prevProgress ) => prevProgress + 1 );
					await new Promise( resolve => setTimeout( resolve, scanInterval / extractedLinks.length ) ); // Simulate network latency
				}
				setLinks( results );
			} catch ( e ) {
				setError( e.message || __( 'An unknown error occurred.', 'custom-link-checker' ) );
			} finally {
				setChecking( false );
			}
		};

		// Effect to trigger scan on load if enabled
		useEffect( () => {
			if ( scanOnLoad ) {
				handleScan();
			}
		}, [ scanOnLoad ] ); // Re-run if scanOnLoad changes

		return (
			<div { ...blockProps }>
				<div x-data="{ open: false }">
					<div className="clc-controls">
						<h3>{ __( 'Broken Link Checker', 'custom-link-checker' ) }</h3>
						<ToggleControl
							label={ __( 'Scan on Load', 'custom-link-checker' ) }
							checked={ scanOnLoad }
							onChange={ ( value ) => setAttributes( { scanOnLoad: value } ) }
						/>
						<TextControl
							label={ __( 'Scan Interval (ms)', 'custom-link-checker' ) }
							type="number"
							value={ scanInterval }
							onChange={ ( value ) => setAttributes( { scanInterval: parseInt( value, 10 ) || 30000 } ) }
							help={ __( 'Time between link checks in milliseconds.', 'custom-link-checker' ) }
						/>
						<Button
							variant="primary"
							onClick={ handleScan }
							disabled={ checking }
						>
							{ checking ? <Spinner /> : __( 'Scan Links', 'custom-link-checker' ) }
						</Button>
					</div>

					{ checking && (
						<div className="clc-progress">
							<p>{ __( 'Scanning:', 'custom-link-checker' ) } { progress } / { totalLinks }</p>
							<progress value={ progress } max={ totalLinks }></progress>
						</div>
					) }

					{ error && (
						<div className="clc-error">
							<p>{ __( 'Error:', 'custom-link-checker' ) } { error }</p>
						</div>
					) }

					{ ! checking && links.length > 0 && (
						<div className="clc-results">
							<h4>{ __( 'Scan Results', 'custom-link-checker' ) }</h4>
							<button className="clc-toggle-results" onClick={ () => Alpine.store('clc_results_visible', !Alpine.store('clc_results_visible')) }>
								{ __( 'Show/Hide Results', 'custom-link-checker' ) }
							</button>
							<div x-show="$store.clc_results_visible">
								<ul>
									{ links.map( ( link, index ) => (
										<li key={ index } className={ link.status === 'broken' ? 'clc-broken' : 'clc-ok' }>
											<a href={ link.url } target="_blank" rel="noopener noreferrer">{ link.url }</a>
											<span>({ link.status === 'broken' ? __( 'Broken', 'custom-link-checker' ) : __( 'OK', 'custom-link-checker' ) } - { link.statusCode })</span>
										</li>
									) ) }
								</ul>
							</div>
						</div>
					) }
				</div>
			</div>
		);
	},
	save: ( { attributes } ) => {
		const blockProps = useBlockProps.save();
		// The save function should return null or a static representation
		// as the link checking logic is client-side and dynamic.
		// We can optionally render a placeholder or instructions.
		return (
			<div { ...blockProps }>
				<p>{ __( 'Broken Link Checker block. Results will appear dynamically in the editor.', 'custom-link-checker' ) }</p>
			</div>
		);
	},
} );

Alpine.js Integration and State Management

While the above uses React hooks for state management within the Gutenberg editor (as is standard), the Alpine.js directives are embedded directly into the JSX for demonstration purposes. In a production scenario, you would typically load Alpine.js separately and initialize it on a specific DOM element. For this block, we’ll simulate Alpine.js behavior using React state and conditionally render elements based on those states. The x-data and x-show attributes are illustrative of how Alpine.js would manage UI state.

A more robust implementation would involve a server-side endpoint to perform the actual link checks, preventing CORS issues and improving performance. The JavaScript would then make an AJAX request to this endpoint.

Styling (src/style.scss and src/editor.scss)

Create src/style.scss for frontend styles and src/editor.scss for editor-specific styles. These will be compiled into build/style-index.css and build/index.css respectively.

/* src/style.scss */
.wp-block-custom-link-checker-broken-link-checker {
	border: 1px solid #ccc;
	padding: 15px;
	margin-bottom: 15px;
	background-color: #f9f9f9;

	.clc-results ul {
		list-style: none;
		padding: 0;
		margin-top: 10px;
	}

	.clc-results li {
		margin-bottom: 5px;
		padding: 5px;
		border-radius: 3px;
	}

	.clc-ok {
		background-color: #e6ffed;
		color: #28a745;
	}

	.clc-broken {
		background-color: #ffebee;
		color: #dc3545;
	}

	.clc-results span {
		font-size: 0.9em;
		margin-left: 10px;
	}
}
/* src/editor.scss */
.wp-block-custom-link-checker-broken-link-checker {
	.clc-controls {
		margin-bottom: 15px;
		padding-bottom: 15px;
		border-bottom: 1px solid #eee;
	}

	.clc-controls h3 {
		margin-top: 0;
	}

	.clc-progress {
		margin-top: 15px;
		text-align: center;
	}

	.clc-error {
		margin-top: 15px;
		color: #dc3545;
		font-weight: bold;
	}

	.clc-toggle-results {
		background: none;
		border: none;
		color: #0073aa;
		cursor: pointer;
		padding: 0;
		font-size: 1em;
		margin-bottom: 10px;
		text-decoration: underline;
	}
}

Building the Block Assets

With the JavaScript and SCSS files in place, navigate to your plugin’s root directory in the terminal and run the build command:

npm run build

This command will compile your SCSS files and transpile your JavaScript, creating the necessary files in the build/ directory. If you want to watch for changes during development, use:

npm run start

Server-Side Link Checking (Recommended)

The client-side JavaScript approach for link checking has limitations, primarily due to browser security policies (CORS) that prevent direct AJAX requests to arbitrary external URLs. For a production-ready solution, you must implement a server-side endpoint to perform the HTTP requests.

Creating a REST API Endpoint

Add the following to your custom-link-checker.php file to create a REST API endpoint:

/**
 * Register REST API endpoint for link checking.
 */
function clc_register_rest_endpoint() {
	register_rest_route( 'custom-link-checker/v1', '/check-links', array(
		'methods'             => 'POST',
		'callback'            => 'clc_check_links_callback',
		'permission_callback' => function () {
			// Ensure only logged-in users with sufficient permissions can access.
			return current_user_can( 'edit_posts' );
		},
		'args'                => array(
			'content' => array(
				'required'          => true,
				'type'              => 'string',
				'sanitize_callback' => 'wp_kses_post',
			),
		),
	) );
}
add_action( 'rest_api_init', 'clc_register_rest_endpoint' );

/**
 * Callback function for the link checking REST API endpoint.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response Response object.
 */
function clc_check_links_callback( WP_REST_Request $request ) {
	$content = $request->get_param( 'content' );
	if ( empty( $content ) ) {
		return new WP_Error( 'invalid_content', __( 'Content is required.', 'custom-link-checker' ), array( 'status' => 400 ) );
	}

	// Use DOMDocument to parse HTML and extract links.
	$dom = new DOMDocument();
	// Suppress warnings for malformed HTML.
	@$dom->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ) );
	$links = array();

	$anchor_tags = $dom->getElementsByTagName( 'a' );
	foreach ( $anchor_tags as $tag ) {
		$href = $tag->getAttribute( 'href' );
		if ( ! empty( $href ) && ( strpos( $href, 'http://' ) === 0 || strpos( $href, 'https://' ) === 0 ) ) {
			$links[] = esc_url_raw( $href ); // Sanitize URL
		}
	}

	// Remove duplicates
	$links = array_unique( $links );

	$results = array();
	$timeout = 5; // seconds

	foreach ( $links as $url ) {
		$response_code = null;
		$error_message = null;

		$args = array(
			'timeout' => $timeout,
			'sslverify' => false, // Consider security implications
			'user-agent' => 'WordPress Broken Link Checker Bot',
		);

		// Use wp_remote_head for efficiency, fall back to wp_remote_get if needed.
		$response = wp_remote_head( $url, $args );

		if ( is_wp_error( $response ) ) {
			$error_message = $response->get_error_message();
			$response_code = 'error';
		} else {
			$response_code = wp_remote_retrieve_response_code( $response );
		}

		$results[] = array(
			'url'  => $url,
			'status' => ( $response_code >= 400 || $response_code === 'error' ) ? 'broken' : 'ok',
			'statusCode' => $response_code,
			'errorMessage' => $error_message,
		);
	}

	return new WP_REST_Response( $results, 200 );
}

Updating the Editor JavaScript

Modify the handleScan function in src/index.js to use the new REST API endpoint:

// ... inside the edit function ...

		const handleScan = async () => {
			setChecking( true );
			setError( null );
			setLinks( [] );
			setProgress( 0 );
			setTotalLinks( 0 );

			try {
				// Get current post content (this is a simplified approach)
				// In a real scenario, you'd likely need to get the *saved* post content
				// or use a more robust method to capture all block content.
				// For this example, we'll assume we can get the content.
				// A more accurate way might involve fetching the post content via AJAX
				// or using the Block Editor's internal state if available.

				// For demonstration, let's simulate getting content.
				// In a real plugin, you'd likely pass the post ID and fetch content server-side,
				// or use a method to serialize the current editor state.
				// Here, we'll just use a placeholder and assume the server endpoint handles it.
				// A better approach for the editor would be to serialize the blocks and send.
				// For simplicity, we'll just send a dummy content string and rely on the server
				// to parse the *actual* post content if it were a real AJAX call from the frontend.
				// Or, better yet, pass the current editor content to the endpoint.

				// Let's try to get the current editor content. This is tricky as it's dynamic.
				// A more reliable method is to fetch the post content via AJAX using the post ID.
				// For this example, we'll simulate sending the content.
				// A robust solution would involve `wp.data.select('core/editor').getEditedPostContent()`
				// but this requires careful handling of serialization and AJAX.

				// Let's simulate fetching the content for the AJAX call.
				// In a real scenario, you'd get the current post's content.
				// For this example, we'll use a placeholder and assume the server endpoint
				// would receive the actual content to parse.
				// A more practical approach for the editor:
				const currentPostContent = wp.data.select('core/editor').getEditedPostContent();

				const response = await fetch( '/wp-json/custom-link-checker/v1/check-links', {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce': wpApiSettings.nonce, // WordPress REST API nonce
					},
					body: JSON.stringify( { content: currentPostContent } ),
				} );

				if ( ! response.ok ) {
					const errorData = await response.json();
					throw new Error( errorData.message || __( 'Failed to check links.', 'custom-link-checker' ) );
				}

				const data = await response.json();
				setLinks( data );
				setTotalLinks( data.length ); // Assuming the response contains all links checked
				setProgress( data.length ); // Assuming all links are processed by the time response is received
			} catch ( e ) {
				setError( e.message || __( 'An unknown error occurred.', 'custom-link-checker' ) );
			} finally {
				setChecking( false );
			}
		};

		// ... rest of the edit function ...

Note the use of wpApiSettings.nonce for authentication with the WordPress REST API. Ensure this is correctly enqueued in your PHP file if not already available.

Conclusion and Further Enhancements

This guide provides a foundational custom Gutenberg block for broken link checking, integrating client-side state management concepts with a robust server-side API for actual checks. The use of Alpine.js principles (even if simulated with React state in the editor) allows for a dynamic and interactive user experience.

Further Enhancements:

  • Background Scanning: Implement a WordPress cron job or WP-CLI command for periodic background scans of published posts.
  • Link Management Interface: Create a dedicated admin page to list all broken links across the site, with options to edit or ignore them.
  • External Link Checking: Integrate with third-party APIs for more comprehensive link validation.
  • Performance Optimization: For very large posts, consider debouncing the scan input and optimizing the server-side parsing and request handling.
  • User Feedback: Provide more detailed feedback during the scanning process, including specific error messages from the server.

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 analyze and reduce CPU consumption of custom Adapter and Decorator patterns event mediators
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to custom subscription logs
  • Debugging Guide: Diagnosing cURL socket timeout limits in multi-site network environments with modern tools
  • Debugging and Resolving complex hook execution order overrides issues during heavy concurrent database traffic
  • Troubleshooting namespace class loading collisions in production when using modern Timber Twig templating engines wrappers

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 (42)
  • 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 (103)
  • WordPress Plugin Development (107)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • How to analyze and reduce CPU consumption of custom Adapter and Decorator patterns event mediators
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to custom subscription logs
  • Debugging Guide: Diagnosing cURL socket timeout limits in multi-site network environments with modern tools

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