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('')}
Important Considerations for src/index.js:
- Web Component Definition: The
BrokenLinkCheckerclass extendsHTMLElement. It uses Shadow DOM for encapsulation. - State Management: Basic state is managed within the component’s
this.stateobject. - Attribute Handling:
observedAttributesandattributeChangedCallbackare used to react to changes in attributes passed from the Gutenberg block (likescanIntervalandlastScan). - Link Extraction:
extractLinksusesDOMParserto find all<a>tags. - Link Checking: The
checkLinksfunction usesfetchwithHEADmethod andno-corsmode. Crucially,no-corsmode 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).
- Gutenberg Integration:
- The
editfunction renders a placeholder<div>and usesuseEffectto mount and manage the<broken-link-checker>Web Component within it. useRefis used to get a reference to the DOM element.useEffectis used to synchronize attributes from Gutenberg to the Web Component and to listen for attribute changes from the Web Component back to Gutenberg (via aMutationObserverin this simplified example, though custom events are preferred).- The
savefunction returnsnull, indicating this is a dynamic block or that its rendering is entirely client-side viaeditorScript.
- The
- Styling: Styles are included within the Web Component’s Shadow DOM and also in
editor.scssandstyle.scssfor 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.