• 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 Tailwind CSS isolated elements

Step-by-Step Guide to building a custom broken link checker block for Gutenberg using Tailwind CSS isolated elements

Plugin Scaffolding and Dependencies

We’ll begin by setting up the basic plugin structure and defining our JavaScript and CSS dependencies. For this custom Gutenberg block, we’ll leverage the WordPress `@wordpress/scripts` package for build tooling, which simplifies compilation of modern JavaScript and CSS. We’ll also explicitly enqueue Tailwind CSS, but in an isolated manner to prevent conflicts with other themes or plugins.

Create a new directory for your plugin, e.g., broken-link-checker-block, within your WordPress installation’s wp-content/plugins/ directory. Inside this directory, create the main plugin file, broken-link-checker-block.php.

Main Plugin File (broken-link-checker-block.php)

This file will contain the plugin header and the necessary hooks to register our Gutenberg block and enqueue our assets.

<?php
/**
 * Plugin Name: Broken Link Checker Block
 * Description: A custom Gutenberg block to check for broken links within a post.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: broken-link-checker-block
 */

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

/**
 * Registers the block using the metadata loaded from the `block.json` file.
 * Behind the scenes, it registers also all assets so they can be enqueued
 * through the block editor in the corresponding context.
 *
 * @see https://developer.wordpress.org/reference/functions/register_block_type/
 */
function broken_link_checker_block_init() {
	register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'broken_link_checker_block_init' );

/**
 * Enqueue Tailwind CSS in an isolated manner for the block editor.
 */
function broken_link_checker_block_enqueue_editor_assets() {
	// Enqueue Tailwind CSS.
	wp_enqueue_style(
		'broken-link-checker-tailwind',
		plugin_dir_url( __FILE__ ) . 'build/tailwind.css',
		array(),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/tailwind.css' )
	);

	// Enqueue the block's JavaScript and CSS.
	wp_enqueue_script(
		'broken-link-checker-block-editor-script',
		plugin_dir_url( __FILE__ ) . 'build/index.js',
		array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
	);
}
add_action( 'enqueue_block_editor_assets', 'broken_link_checker_block_enqueue_editor_assets' );

/**
 * Enqueue Tailwind CSS for the frontend.
 * This is optional, depending on whether you want the block's styling to appear on the frontend.
 */
function broken_link_checker_block_enqueue_frontend_assets() {
	wp_enqueue_style(
		'broken-link-checker-tailwind-frontend',
		plugin_dir_url( __FILE__ ) . 'build/tailwind.css',
		array(),
		filemtime( plugin_dir_path( __FILE__ ) . 'build/tailwind.css' )
	);
}
// Uncomment the line below if you want Tailwind CSS to be enqueued on the frontend as well.
// add_action( 'wp_enqueue_scripts', 'broken_link_checker_block_enqueue_frontend_assets' );

Build Tooling Setup with `@wordpress/scripts`

To manage our JavaScript and CSS compilation, we’ll use the `@wordpress/scripts` package. This provides a convenient way to handle modern JS features, JSX, and CSS preprocessing. First, initialize npm in your plugin directory:

cd broken-link-checker-block
npm init -y

Next, install the necessary development dependencies:

npm install @wordpress/scripts --save-dev

Now, configure your package.json to include build scripts. We’ll add scripts for development (watching for changes) and production builds.

package.json Configuration

{
  "name": "broken-link-checker-block",
  "version": "1.0.0",
  "description": "A custom Gutenberg block to check for broken links within a post.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "package": "wp-scripts start --output-path=build --packages=wp-scripts,react,react-dom"
  },
  "keywords": [
    "wordpress",
    "gutenberg",
    "block"
  ],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.10.0"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

The "start" script will watch for changes and recompile assets automatically, ideal for development. The "build" script creates optimized production-ready assets.

Tailwind CSS Integration (Isolated)

To integrate Tailwind CSS without global scope pollution, we’ll use its PostCSS configuration to generate a CSS file that only includes the necessary styles for our block. This requires a tailwind.config.js file and a postcss.config.js file.

Tailwind Configuration (tailwind.config.js)

Create tailwind.config.js in the root of your plugin directory. We’ll configure the content to scan only our block’s JavaScript files for class usage. This is crucial for keeping the generated CSS minimal.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/**/*.js', // Scan JS files in src directory
    './block.json',  // Scan block.json for potential class usage
  ],
  theme: {
    extend: {},
  },
  plugins: [],
  // Prefix all generated classes to ensure isolation
  prefix: 'blc-',
  // Add a safelist to ensure essential classes are not purged
  safelist: [
    'blc-text-red-500',
    'blc-text-green-500',
    'blc-font-bold',
    'blc-p-4',
    'blc-rounded',
    'blc-shadow-md',
    'blc-bg-gray-100',
    'blc-border',
    'blc-border-gray-300',
    'blc-text-sm',
    'blc-mb-2',
    'blc-w-full',
    'blc-h-10',
    'blc-px-3',
    'blc-py-2',
    'blc-rounded-md',
    'blc-bg-blue-500',
    'blc-text-white',
    'blc-hover:bg-blue-700',
    'blc-cursor-pointer',
    'blc-text-gray-700',
    'blc-text-lg',
    'blc-mt-4',
    'blc-space-y-4',
    'blc-text-center',
    'blc-animate-spin',
    'blc-opacity-50',
  ],
}

The prefix: 'blc-' option is vital. It prepends all Tailwind classes with blc-, ensuring that styles only apply to elements within our block and don’t interfere with other parts of the site. The safelist is a fallback to prevent essential classes from being purged during the build process, especially if they are dynamically generated or used in ways that static analysis might miss.

PostCSS Configuration (postcss.config.js)

Create postcss.config.js in the root of your plugin directory. This file tells `@wordpress/scripts` how to process your CSS, including Tailwind.

module.exports = {
  plugins: {
    'tailwindcss/nesting': {},
    'tailwindcss': {},
    'autoprefixer': {},
  },
};

Now, create a src directory in your plugin’s root. Inside src, create a file named editor.scss. This will be our main SCSS file where we import Tailwind and define our block’s styles.

SCSS Entry Point (src/editor.scss)

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom styles for the broken link checker block */
.wp-block-broken-link-checker-block {
  @apply blc-p-4 blc-bg-gray-100 blc-rounded blc-shadow-md blc-border blc-border-gray-300;

  h3 {
    @apply blc-text-lg blc-font-bold blc-mb-2 blc-text-gray-700;
  }

  .blc-status-message {
    @apply blc-text-sm blc-mb-2;
  }

  .blc-checking {
    @apply blc-text-blue-500 blc-animate-spin blc-opacity-50;
  }

  .blc-ok {
    @apply blc-text-green-500;
  }

  .blc-error {
    @apply blc-text-red-500;
  }

  .blc-button {
    @apply blc-bg-blue-500 blc-text-white blc-font-bold blc-py-2 blc-px-4 blc-rounded blc-hover:bg-blue-700 blc-cursor-pointer blc-mt-4;
  }
}

In this SCSS file, we import Tailwind’s directives. We then apply Tailwind classes (prefixed with blc-) to our block’s wrapper and internal elements. The .wp-block-broken-link-checker-block class is the default wrapper generated by Gutenberg for blocks. We also define specific styles for status messages and the check button.

To ensure these styles are compiled and output to build/tailwind.css, we need to tell `@wordpress/scripts` to process editor.scss. We do this by creating a block.json file in the root of your plugin directory.

Block Metadata (block.json)

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "broken-link-checker-block/broken-link-checker-block",
  "version": "1.0.0",
  "title": "Broken Link Checker",
  "category": "widgets",
  "icon": "admin-links",
  "description": "Checks for broken links within the current post.",
  "attributes": {
    "message": {
      "type": "string",
      "default": "Click the button to check for broken links."
    },
    "status": {
      "type": "string",
      "default": "idle"
    }
  },
  "textdomain": "broken-link-checker-block",
  "editorScript": "file:./build/index.js",
  "editorStyle": "file:./build/tailwind.css",
  "style": "file:./build/style-index.css"
}

Crucially, the editorStyle property points to file:./build/tailwind.css. This tells WordPress to enqueue this CSS file specifically for the block editor. The style property points to style-index.css, which will be generated by `@wordpress/scripts` and can contain frontend styles if needed (though for this example, we’re focusing on the editor).

Now, run the build command:

npm run build

This will create the build directory containing index.js, index.asset.php, and tailwind.css. The tailwind.css file will contain all the compiled Tailwind utilities, prefixed with blc-, and any custom styles defined in editor.scss.

Gutenberg Block Development (src/index.js)

This is where the core logic and UI for our Gutenberg block will reside. We’ll use React components provided by the WordPress block editor API.

src/index.js – Block Registration and Editor Component

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { Button, Spinner, Text } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';

// Import our isolated Tailwind CSS
import './editor.scss';

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

registerBlockType( blockName, {
	edit: function ( { attributes, setAttributes } ) {
		const { message, status } = attributes;

		const [ links, setLinks ] = useState( [] );
		const [ isLoading, setIsLoading ] = useState( false );
		const [ error, setError ] = useState( null );

		// Fetch post content to find links
		const postContent = useSelect( ( select ) => {
			const { getEditedPostContent } = select( coreStore );
			return getEditedPostContent();
		}, [] );

		// Function to extract links from post content
		const extractLinks = ( content ) => {
			const parser = new DOMParser();
			const doc = parser.parseFromString( content, 'text/html' );
			const anchorTags = doc.querySelectorAll( 'a[href]' );
			const extracted = [];
			anchorTags.forEach( ( anchor ) => {
				const href = anchor.getAttribute( 'href' );
				// Basic validation: ignore mailto, tel, and empty hrefs
				if ( href && ! href.startsWith( 'mailto:' ) && ! href.startsWith( 'tel:' ) && href.trim() !== '' ) {
					extracted.push( { url: href, status: 'pending' } );
				}
			} );
			return extracted;
		};

		// Effect to update links when post content changes
		useEffect( () => {
			if ( postContent ) {
				setLinks( extractLinks( postContent ) );
			}
		}, [ postContent ] );

		const checkLinks = async () => {
			setIsLoading( true );
			setError( null );
			setAttributes( { status: 'checking', message: 'Checking links...' } );

			const linksToCheck = extractLinks( postContent );
			if ( linksToCheck.length === 0 ) {
				setAttributes( { status: 'no-links', message: 'No links found in this post.' } );
				setIsLoading( false );
				return;
			}

			const results = [];
			for ( const link of linksToCheck ) {
				try {
					// Use WordPress REST API to check link status.
					// This is a simplified example; a real-world scenario might require a custom endpoint
					// or a more robust external API call. For demonstration, we'll simulate.
					// A common approach is to use wp_remote_head or wp_remote_get.
					// For simplicity here, we'll simulate a response.
					// In a real plugin, you'd make an AJAX request to a custom WP REST API endpoint
					// that performs the actual HEAD request.

					// Simulate an AJAX call to a hypothetical endpoint
					const response = await fetch( '/wp-json/broken-link-checker/v1/check-url', {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json',
						},
						body: JSON.stringify( { url: link.url } ),
					} );

					if ( ! response.ok ) {
						throw new Error( `HTTP error! status: ${ response.status }` );
					}

					const data = await response.json();
					results.push( { ...link, status: data.status } );

				} catch ( e ) {
					console.error( `Error checking link ${ link.url }:`, e );
					results.push( { ...link, status: 'error' } );
					setError( `Failed to check some links. Please try again.` );
				}
			}

			setLinks( results );
			const brokenLinks = results.filter( l => l.status === 'error' );
			if ( brokenLinks.length > 0 ) {
				setAttributes( { status: 'errors', message: `${ brokenLinks.length } broken link(s) found.` } );
			} else {
				setAttributes( { status: 'ok', message: 'All links appear to be working.' } );
			}
			setIsLoading( false );
		};

		const getStatusClass = ( currentStatus ) => {
			switch ( currentStatus ) {
				case 'checking':
					return 'blc-checking';
				case 'ok':
					return 'blc-ok';
				case 'error':
					return 'blc-error';
				default:
					return '';
			}
		};

		return (
			<div className="wp-block-broken-link-checker-block">
				<h3>{ __( 'Broken Link Checker', 'broken-link-checker-block' ) }</h3>
				<p className="blc-status-message">{ message }</p>

				{ isLoading && <Spinner /> }
				{ error && <Text className="blc-error" isError>{ error }</Text> }

				<Button
					variant="primary"
					className="blc-button"
					onClick={ checkLinks }
					disabled={ isLoading || !postContent }
				>
					{ __( 'Check Links', 'broken-link-checker-block' ) }
				</Button>

				{ !isLoading && links.length > 0 && (
					<div className="blc-mt-4">
						<h4>{ __( 'Link Status:', 'broken-link-checker-block' ) }</h4>
						<ul className="blc-space-y-2">
							{ links.map( ( link, index ) => (
								<li key={ index } className={ `blc-text-sm ${ getStatusClass( link.status ) }` }>
									<a href={ link.url } target="_blank" rel="noopener noreferrer">{ link.url }</a>
									{ link.status === 'pending' && <span> (Pending) </span> }
									{ link.status === 'error' && <span> (Broken) </span> }
									{ link.status === 'ok' && <span> (OK) </span> }
								</li>
							) ) }
						</ul>
					</div>
				) }
			</div>
		);
	},

	save: function ( { attributes } ) {
		const { message, status } = attributes;
		// The save function should return an empty div or static HTML.
		// The actual link checking and display will happen in the editor.
		// For frontend display, you might want a separate mechanism or a simplified view.
		// For this example, we'll just show the last message.
		return (
			<div className="wp-block-broken-link-checker-block">
				<p className="blc-status-message">{ message }</p>
				{ status === 'checking' && <p>Checking links...</p> }
				{ status === 'errors' && <p className="blc-error">Broken links detected. Check the editor for details.</p> }
				{ status === 'ok' && <p className="blc-ok">All links checked and seem fine.</p> }
			</div>
		);
	},
} );

Explanation of src/index.js

  • Imports: We import necessary components from `@wordpress/blocks`, `@wordpress/i18n`, `@wordpress/data`, and `@wordpress/components`. We also import our SCSS file.
  • `registerBlockType`: This function registers our block with WordPress, using the name defined in block.json.
  • `edit` function: This is the React component that renders the block in the Gutenberg editor.
    • State Management: We use useState for managing the list of links, loading state, and any errors.
    • `useSelect` hook: This hook from `@wordpress/data` allows us to access the current post’s content as it’s being edited.
    • `extractLinks` function: A utility function that parses the HTML content of the post and extracts all valid `` tag `href` attributes. It performs basic filtering to exclude `mailto:` and `tel:` links.
    • `useEffect` hook: This hook watches for changes in postContent and updates the internal links state.
    • `checkLinks` function: This is the core logic. It extracts links, simulates an AJAX call to a hypothetical WordPress REST API endpoint (/wp-json/broken-link-checker/v1/check-url) for each URL, and updates the UI with the results.
    • `getStatusClass` function: A helper to apply specific Tailwind CSS classes based on the link status.
    • UI Components: We use Button for the “Check Links” action, Spinner for loading indication, and Text for displaying errors. Tailwind classes are applied via the className prop.
  • `save` function: This function defines how the block’s content is saved to the database. For this block, we’re primarily focused on the editor experience. The save function returns minimal static HTML, indicating the status of the last check. A more advanced implementation might fetch and display results on the frontend, but that would typically involve a separate AJAX call on page load.

Backend API Endpoint for Link Checking

The JavaScript code above assumes a REST API endpoint exists at /wp-json/broken-link-checker/v1/check-url that accepts a POST request with a JSON body containing a url. This endpoint needs to be created within your plugin.

Custom REST API Endpoint (broken-link-checker-block.php)

Add the following code to your main plugin file (broken-link-checker-block.php) to register this endpoint.

 'POST',
		'callback'            => 'broken_link_checker_check_url_callback',
		'permission_callback' => function() {
			// Ensure only logged-in users can access this endpoint.
			// You might want to refine this based on user capabilities.
			return current_user_can( 'edit_posts' );
		},
		'args'                => array(
			'url' => array(
				'required'          => true,
				'type'              => 'string',
				'validate_callback' => function( $param, $request, $key ) {
					return filter_var( $param, FILTER_VALIDATE_URL );
				},
			),
		),
	) );
}
add_action( 'rest_api_init', 'broken_link_checker_register_rest_route' );

/**
 * Callback function for the REST API endpoint to check URL status.
 *
 * @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 broken_link_checker_check_url_callback( WP_REST_Request $request ) {
	$url = $request->get_param( 'url' );

	// Use WordPress HTTP API to check the URL.
	// wp_remote_head is generally preferred for checking link validity as it's faster.
	$response = wp_remote_head( $url, array( 'timeout' => 5 ) );

	if ( is_wp_error( $response ) ) {
		return new WP_Error( 'url_check_failed', __( 'Failed to connect to the URL.', 'broken-link-checker-block' ), array( 'status' => 500, 'error_message' => $response->get_error_message() ) );
	}

	$status_code = wp_remote_retrieve_response_code( $response );

	// Consider codes in the 2xx and 3xx range as successful (3xx are redirects, which are usually fine).
	if ( $status_code >= 200 && $status_code < 400 ) {
		return rest_ensure_response( array( 'status' => 'ok' ) );
	} else {
		return rest_ensure_response( array( 'status' => 'error', 'status_code' => $status_code ), 200 ); // Return 200 OK to JS, but indicate error status
	}
}

This code registers a new REST API namespace and route. The broken_link_checker_check_url_callback function uses wp_remote_head to perform an HTTP HEAD request to the provided URL. It returns a JSON response indicating whether the link is ‘ok’ or ‘error’, along with the HTTP status code if it’s an error. The permission_callback ensures that only users with the `edit_posts` capability can access this endpoint, preventing abuse.

Building and Activation

After setting up all the files, navigate to your plugin’s directory in the terminal and run the build command:

npm run build

This will compile your JavaScript and SCSS into the build directory. Then, go to your WordPress admin area, navigate to “Plugins,” and activate the “Broken Link Checker Block” plugin.

Using the Block

Now, when you edit a post or page in Gutenberg, you can add the “Broken Link Checker” block. Click the “Check Links” button. The block will analyze the post content, make requests to our custom REST API endpoint, and display the status of each link, highlighting any broken ones. The Tailwind CSS classes, prefixed with blc-, will style the block elements, ensuring isolation.

Further Enhancements

  • Frontend Display: Implement a mechanism to display link check results on the frontend, perhaps via a shortcode or a dedicated frontend rendering of the block.
  • AJAX for Frontend: On page load, trigger an AJAX request to the backend to fetch the latest link check results for the post and display them.
  • Customizable Thresholds: Allow users to define what constitutes a “broken” link (e.g., specific HTTP status codes).
  • Background Processing: For posts with many links, consider using WordPress Cron or background job queues for link checking to avoid timeouts and improve user experience.
  • External API Integration: For more robust checking (e.g., handling complex redirects, CAPTCHAs), integrate with a dedicated broken link checking API service.
  • Link Filtering: Add options to exclude certain URLs or URL patterns from being checked.
  • More Granular Permissions: Refine the permission_callback for the REST API to allow specific user roles.

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 securely integrate AWS S3 file uploads endpoints into WordPress custom plugins using WP HTTP API
  • How to build custom FSE Block Themes extensions utilizing modern WordPress Settings API schemas
  • Troubleshooting nonce validation collisions in production when using modern Carbon Fields custom wrappers wrappers
  • Implementing automated compliance reporting for custom member profile directories ledgers using custom PhpSpreadsheet components
  • How to build custom FSE Block Themes extensions utilizing modern Heartbeat API schemas

Categories

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

Recent Posts

  • How to securely integrate AWS S3 file uploads endpoints into WordPress custom plugins using WP HTTP API
  • How to build custom FSE Block Themes extensions utilizing modern WordPress Settings API schemas
  • Troubleshooting nonce validation collisions in production when using modern Carbon Fields custom wrappers wrappers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (818)
  • Debugging & Troubleshooting (603)
  • Security & Compliance (580)
  • 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