• 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 Vanilla JS Web Components

Step-by-Step Guide to building a custom broken link checker block for Gutenberg using Vanilla JS Web Components

Project Setup and Environment Configuration

This guide details the creation of a custom Gutenberg block for WordPress that functions as a broken link checker. We’ll leverage Vanilla JavaScript and Web Components for a modern, framework-agnostic approach. The focus is on a robust, production-ready implementation. We’ll begin by setting up the necessary development environment and project structure.

For this project, we’ll use Node.js and npm for package management and build tooling. Ensure you have Node.js (version 14 or higher recommended) and npm installed. A basic WordPress development environment (e.g., Local by Flywheel, Docker, or a local Apache/Nginx setup) is also required.

Gutenberg Block Structure and `block.json`

Every Gutenberg block requires a `block.json` file to define its metadata, script dependencies, and editor assets. We’ll create a simple structure for our broken link checker block.

Navigate to your WordPress plugin directory (e.g., wp-content/plugins/) and create a new directory for your plugin, say broken-link-checker-block. Inside this directory, create the block.json file.

The block.json will look like this:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "custom-blocks/broken-link-checker",
  "version": "0.1.0",
  "title": "Broken Link Checker",
  "category": "widgets",
  "icon": "admin-links",
  "description": "A custom block to check for broken links within the post content.",
  "keywords": [ "links", "checker", "broken", "utility" ],
  "attributes": {
    "scanInterval": {
      "type": "number",
      "default": 24
    },
    "lastScan": {
      "type": "string",
      "default": ""
    }
  },
  "supports": {
    "html": false
  },
  "textdomain": "broken-link-checker-block",
  "editorScript": "file:./build/index.js",
  "editorStyle": "file:./build/index.css",
  "style": "file:./build/style-index.css"
}

Frontend and Editor Script Compilation

We need to compile our JavaScript and CSS. For this, we’ll use `@wordpress/scripts`, a package that provides a pre-configured build process. Initialize your project with npm and install the necessary dependencies.

In your plugin directory (broken-link-checker-block), run:

npm init -y
npm install @wordpress/scripts --save-dev

Add the following scripts to your package.json:

{
  // ... other package.json content
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "packages-update": "wp-scripts packages-update"
  }
  // ...
}

Now, create the necessary source files:

  • src/index.js: The main entry point for the block’s editor and frontend scripts.
  • src/editor.scss: Styles for the block in the editor.
  • src/style.scss: Styles for the block on the frontend.

The block.json references build/index.js and build/index.css. Running npm start will watch for changes and compile your assets into the build directory. Running npm run build will create production-ready minified assets.

Implementing the Block with Web Components

We’ll define our block’s behavior using a JavaScript class that extends HTMLElement. This class will encapsulate the block’s logic, including the link scanning functionality.

First, let’s set up the basic structure in src/index.js. We’ll register the block using registerBlockType from @wordpress/blocks and define its edit and save functions. The edit function will render our Web Component in the editor.

src/index.js:

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import './editor.scss';
import './style.scss';

// Define the Web Component
class BrokenLinkChecker extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.state = {
            links: [],
            scanning: false,
            lastScan: this.getAttribute('lastScan') || '',
            scanInterval: parseInt(this.getAttribute('scanInterval'), 10) || 24,
        };
        this.render();
    }

    connectedCallback() {
        this.scanButton = this.shadowRoot.getElementById('scan-button');
        this.scanButton.addEventListener('click', this.handleScan.bind(this));
        this.updateIntervalInput();
        this.shadowRoot.getElementById('interval-input').addEventListener('change', this.handleIntervalChange.bind(this));
        this.loadLinks();
    }

    disconnectedCallback() {
        this.scanButton.removeEventListener('click', this.handleScan.bind(this));
    }

    static get observedAttributes() {
        return ['lastScan', 'scanInterval'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'lastScan') {
            this.state.lastScan = newValue;
        } else if (name === 'scanInterval') {
            this.state.scanInterval = parseInt(newValue, 10) || 24;
            this.updateIntervalInput();
        }
        this.render(); // Re-render to reflect attribute changes
    }

    updateIntervalInput() {
        const intervalInput = this.shadowRoot.getElementById('interval-input');
        if (intervalInput) {
            intervalInput.value = this.state.scanInterval;
        }
    }

    async handleScan() {
        this.state.scanning = true;
        this.render();

        const postContent = document.querySelector('.editor-post-edit-content').__editor.getRawPostContent(); // Accessing internal editor state - use with caution or find a public API if available.
        const links = this.extractLinks(postContent);
        this.state.links = links.map(link => ({ url: link, status: 'checking' }));
        this.render();

        const results = await this.checkLinks(links);
        this.state.links = results;
        this.state.scanning = false;
        this.state.lastScan = new Date().toISOString();
        this.updateAttributes();
        this.render();
    }

    extractLinks(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const anchors = doc.querySelectorAll('a[href]');
        const urls = [];
        anchors.forEach(a => {
            const href = a.getAttribute('href');
            if (href && href.startsWith('http')) { // Basic check for external links
                urls.push(href);
            }
        });
        return [...new Set(urls)]; // Return unique URLs
    }

    async checkLinks(urls) {
        const results = [];
        for (const url of urls) {
            try {
                const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); // HEAD is more efficient, no-cors for cross-origin
                // Note: 'no-cors' mode means we can't read the response body or status directly.
                // A more robust solution would involve a server-side check or a CORS-enabled API.
                // For this example, we'll simulate a check. In a real-world scenario,
                // you'd likely send these URLs to a backend endpoint for checking.
                const isBroken = response.type === 'opaque' && !response.ok; // Heuristic for no-cors
                results.push({ url, status: isBroken ? 'broken' : 'ok' });
            } catch (error) {
                results.push({ url, status: 'error' });
            }
        }
        return results;
    }

    handleIntervalChange(event) {
        const newInterval = parseInt(event.target.value, 10);
        if (!isNaN(newInterval) && newInterval > 0) {
            this.state.scanInterval = newInterval;
            this.updateAttributes();
            this.render();
        }
    }

    updateAttributes() {
        this.setAttribute('scanInterval', this.state.scanInterval);
        this.setAttribute('lastScan', this.state.lastScan);
    }

    loadLinks() {
        // In a real application, you'd fetch saved link data from post meta or options.
        // For this example, we'll just use the initial state.
        this.render();
    }

    render() {
        const { links, scanning, lastScan, scanInterval } = this.state;
        const formattedLastScan = lastScan ? new Date(lastScan).toLocaleString() : __('Never', 'broken-link-checker-block');

        this.shadowRoot.innerHTML = `
            
            
${__('Scan every', 'broken-link-checker-block')} ${__('hours', 'broken-link-checker-block')}
${__('Last Scan:', 'broken-link-checker-block')} ${formattedLastScan}
${links.length > 0 ? `

Results:

    ${links.map(link => `
  • ${link.url} - ${link.status.charAt(0).toUpperCase() + link.status.slice(1)}
  • `).join('')}
` : ''} `; } } // Define the custom element if (!customElements.get('broken-link-checker')) { customElements.define('broken-link-checker', BrokenLinkChecker); } // Register the Gutenberg block registerBlockType('custom-blocks/broken-link-checker', { edit: ({ attributes, setAttributes }) => { const { scanInterval, lastScan } = attributes; // Use a div as a placeholder and mount the Web Component into it. // This is a common pattern when integrating Web Components with React-based frameworks like Gutenberg. const ref = wp.element.useRef(); wp.element.useEffect(() => { const wc = ref.current.querySelector('broken-link-checker'); if (wc) { wc.setAttribute('scanInterval', scanInterval); wc.setAttribute('lastScan', lastScan); } }, [scanInterval, lastScan]); // Update attributes when the Web Component emits changes wp.element.useEffect(() => { const wc = ref.current.querySelector('broken-link-checker'); const handleAttributeChange = (event) => { const { scanInterval: newInterval, lastScan: newLastScan } = event.target.dataset; // Assuming WC sets data attributes setAttributes({ scanInterval: parseInt(newInterval, 10), lastScan: newLastScan }); }; // Custom event listener for attribute updates from Web Component // This requires the Web Component to dispatch custom events or expose a method. // For simplicity here, we'll rely on attributeChangedCallback and direct attribute setting. // A more robust approach would involve custom events. // Let's simulate attribute updates by observing changes on the WC itself. const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'scaninterval') { setAttributes({ scanInterval: parseInt(mutation.target.getAttribute('scanInterval'), 10) }); } if (mutation.type === 'attributes' && mutation.attributeName === 'lastscan') { setAttributes({ lastScan: mutation.target.getAttribute('lastScan') }); } }); }); if (wc) { observer.observe(wc, { attributes: true }); } return () => observer.disconnect(); }, []); return ( <div ref={ref}> <broken-link-checker scanInterval={scanInterval} lastScan={lastScan} /> </div> ); }, save: ({ attributes }) => { const { scanInterval, lastScan } = attributes; // The save function should return null for dynamic blocks, // or the static HTML for static blocks. // Since our Web Component is dynamic and relies on client-side JS, // we'll return null and register it as a dynamic block server-side if needed, // or rely on the editorScript to render it. // For a truly static save, you'd render the initial HTML structure here. // However, for a functional checker, it's best handled client-side. // We'll return null and ensure editorScript loads it. // If you need it on the frontend too, you'd register a frontend script. // For this example, we'll render the Web Component in the editor and rely on // the editorScript to load it. For frontend rendering, a separate frontendScript // would be needed, or the editorScript could be enqueued for both. // Let's assume for now it's an editor-only tool. return null; // Or return the static HTML structure if it were a static block. }, });

Important Considerations for src/index.js:

  • Web Component Definition: The BrokenLinkChecker class extends HTMLElement. It uses Shadow DOM for encapsulation.
  • State Management: Basic state is managed within the component’s this.state object.
  • Attribute Handling: observedAttributes and attributeChangedCallback are used to react to changes in attributes passed from the Gutenberg block (like scanInterval and lastScan).
  • Link Extraction: extractLinks uses DOMParser to find all <a> tags.
  • Link Checking: The checkLinks function uses fetch with HEAD method and no-cors mode. Crucially, no-cors mode prevents reading the response status directly. This is a significant limitation for client-side broken link checking. A robust solution would involve:
    • A server-side endpoint that receives URLs and performs checks, returning actual status codes.
    • Using a third-party API designed for link checking.
    • Ensuring the target links are on the same origin or have permissive CORS policies (unlikely for general web links).
    For this example, the check is largely simulated or relies on network errors.
  • Gutenberg Integration:
    • The edit function renders a placeholder <div> and uses useEffect to mount and manage the <broken-link-checker> Web Component within it.
    • useRef is used to get a reference to the DOM element.
    • useEffect is used to synchronize attributes from Gutenberg to the Web Component and to listen for attribute changes from the Web Component back to Gutenberg (via a MutationObserver in this simplified example, though custom events are preferred).
    • The save function returns null, indicating this is a dynamic block or that its rendering is entirely client-side via editorScript.
  • Styling: Styles are included within the Web Component’s Shadow DOM and also in editor.scss and style.scss for block-level styling.

Styling the Block

We’ll add basic styles for the block in the editor and on the frontend.

src/editor.scss:

.wp-block-custom-blocks-broken-link-checker {
    border: 1px dashed #9b9b9b;
    padding: 15px;
    background-color: #f8f8f8;
}

src/style.scss:

.wp-block-custom-blocks-broken-link-checker {
    border: 1px solid #e0e0e0;
    padding: 15px;
    background-color: #ffffff;
}

Backend Integration (PHP)

To make the block work, we need a PHP file to register the block type and enqueue the compiled assets. Create a main plugin file, e.g., broken-link-checker-block.php.

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

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 `block.json` sections.
 *
 * @see https://developer.wordpress.org/reference/functions/register_block_type/
 */
function custom_blocks_broken_link_checker_block_init() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_blocks_broken_link_checker_block_init' );

Place this PHP file and the block.json, package.json, src/, and build/ directories within a folder (e.g., broken-link-checker-block) inside your wp-content/plugins/ directory. Activate the plugin in the WordPress admin.

Testing and Refinements

After activating the plugin, edit a post or page. You should be able to add the “Broken Link Checker” block. When you click “Scan for Broken Links,” the block will attempt to extract links and check their status. Remember the limitations of client-side checking with no-cors.

Potential Refinements:

  • Server-Side Scanning: Implement a REST API endpoint or AJAX handler in PHP to perform link checks on the server. This is the most reliable method. The JavaScript would then send URLs to this endpoint and receive structured results.
  • User Feedback: Improve the UI to show progress more granularly, handle timeouts, and provide clearer error messages.
  • Link Storage: Store scan results (perhaps in post meta) to avoid re-scanning unnecessarily or to display historical data.
  • Scheduled Scans: Use WordPress cron (WP-Cron) to schedule automatic scans based on the interval set by the user.
  • Link Filtering: Add options to exclude certain domains or URL patterns.
  • Accessibility: Ensure the Web Component and its controls are accessible.
  • Error Handling: Implement more robust error handling for network requests and DOM manipulation.
  • Editor State Access: The method used to access post content (`document.querySelector(‘.editor-post-edit-content’).__editor.getRawPostContent()`) is brittle as it relies on internal editor implementation details. A more stable approach might involve using the block’s `setAttributes` to store content or using a dedicated API if available.

By combining Gutenberg’s block API with the power and encapsulation of Web Components, we can build highly interactive and reusable UI elements for WordPress. The key challenge remains in performing reliable external resource checks from the client-side, which often necessitates a server-side component.

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

  • Debugging and Resolving complex REST API CORS authorization failures issues during heavy concurrent database traffic
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to online course lessons
  • WordPress Development Recipe: Staggered database writes for high-volume custom form fields using Cron API (wp_schedule_event)
  • Step-by-Step Guide: Offloading high-frequency knowledge base document categories metadata writes to a Redis KV store
  • How to analyze and reduce CPU consumption of custom Singleton Registry Pattern event mediators

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 (116)
  • WordPress Plugin Development (125)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging and Resolving complex REST API CORS authorization failures issues during heavy concurrent database traffic
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to online course lessons
  • WordPress Development Recipe: Staggered database writes for high-volume custom form fields using Cron API (wp_schedule_event)

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