• 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 Next.js headless configurations

Step-by-Step Guide to building a custom broken link checker block for Gutenberg using Next.js headless configurations

Setting Up the Headless WordPress Environment

Before diving into the Gutenberg block development, ensure your WordPress instance is configured for headless operation. This typically involves enabling the REST API and potentially setting up an application password for authenticated requests. For this guide, we’ll assume a standard WordPress installation accessible via its REST API endpoints.

The core of our headless setup will be a Next.js application. Initialize a new Next.js project using Create Next App:

npx create-next-app@latest my-headless-wp-app
cd my-headless-wp-app

Next, we need to install the necessary dependencies for making HTTP requests. `axios` is a popular choice:

npm install axios

For interacting with the WordPress REST API, it’s beneficial to have a utility function. Create a file, for example, lib/api.js, to house these functions.

import axios from 'axios';

const WP_API_URL = process.env.NEXT_PUBLIC_WP_API_URL || 'http://your-wordpress-site.com/wp-json/wp/v2';

export async function getPosts() {
  try {
    const response = await axios.get(`${WP_API_URL}/posts?_embed`);
    return response.data;
  } catch (error) {
    console.error('Error fetching posts:', error);
    return [];
  }
}

export async function getPostBySlug(slug) {
  try {
    const response = await axios.get(`${WP_API_URL}/posts?slug=${slug}&_embed`);
    return response.data[0]; // Assuming slug is unique
  } catch (error) {
    console.error(`Error fetching post with slug ${slug}:`, error);
    return null;
  }
}

// Add other API functions as needed (e.g., for pages, custom post types)

Don’t forget to set the NEXT_PUBLIC_WP_API_URL environment variable in your .env.local file:

NEXT_PUBLIC_WP_API_URL=http://your-wordpress-site.com/wp-json/wp/v2

Developing the Gutenberg Block

Gutenberg blocks are developed using JavaScript, often with React. For a headless setup, the block’s functionality will primarily reside in the frontend Next.js application, but we still need a minimal PHP plugin to register the block and define its editor-side interface.

First, create a simple WordPress plugin. Navigate to your WordPress installation’s wp-content/plugins/ directory and create a new folder, e.g., broken-link-checker-block. Inside this folder, create a main PHP file, broken-link-checker-block.php.

<?php
/**
 * Plugin Name: Broken Link Checker Block
 * Description: A custom Gutenberg block to display broken links.
 * Version: 1.0.0
 * Author: Your Name
 */

function register_broken_link_checker_block() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'register_broken_link_checker_block' );
?>

Now, let’s set up the frontend for the block. We’ll use `@wordpress/scripts` for building our JavaScript assets. In your plugin directory (broken-link-checker-block), run:

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

Add a build script to your package.json:

{
  "name": "broken-link-checker-block",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@wordpress/scripts": "^26.7.0"
  }
}

Create a JavaScript file for your block’s source code, e.g., src/index.js. This file will define both the editor and frontend views of your block. For a headless setup, the editor view will be minimal, primarily for configuration, while the actual rendering and logic will be handled by your Next.js app.

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

registerBlockType('my-blocks/broken-link-checker', {
    title: __('Broken Link Checker', 'my-blocks'),
    icon: 'admin-links',
    category: 'widgets',
    attributes: {
        postToCheck: {
            type: 'string',
            default: '',
        },
    },
    edit: ({ attributes, setAttributes }) => {
        const blockProps = useBlockProps();
        const { postToCheck } = attributes;

        return [
            <InspectorControls>
                <PanelBody title={__('Link Checker Settings', 'my-blocks')} initialOpen={true}>
                    <TextControl
                        label={__('Post Slug to Check', 'my-blocks')}
                        value={postToCheck}
                        onChange={(newSlug) => setAttributes({ postToCheck: newSlug })}
                        help={__('Enter the slug of the post you want to check links for.', 'my-blocks')}
                    />
                </PanelBody>
            </InspectorControls>,
            <div {...blockProps}>
                {postToCheck ?
                    __(`Checking links for post: ${postToCheck}`, 'my-blocks') :
                    __('Select a post slug in the block settings.', 'my-blocks')
                }
            </div>,
        ];
    },
    save: () => {
        // In a headless setup, the 'save' function typically returns null
        // as the frontend rendering is handled by the Next.js application.
        // We can use a placeholder or a data attribute to pass information.
        return null;
    },
});

After creating src/index.js, run the build command in your plugin directory:

npm run build

This will generate a build folder containing the compiled JavaScript. Activate your “Broken Link Checker Block” plugin in WordPress. You should now be able to add the block to your posts/pages. Note that the Save function returns null. This is crucial for headless WordPress; the block’s content and functionality won’t be directly saved in the post’s HTML content but rather managed through attributes and rendered on the frontend by your Next.js app.

Frontend Rendering and Link Checking Logic in Next.js

The real work happens in your Next.js application. We need to fetch the block’s attributes and then implement the link checking logic.

First, let’s create a component that will render the block’s content. Create a file like components/BrokenLinkChecker.js.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const BrokenLinkChecker = ({ postSlug }) => {
    const [brokenLinks, setBrokenLinks] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
        if (!postSlug) return;

        const checkLinks = async () => {
            setLoading(true);
            setError(null);
            setBrokenLinks([]);

            try {
                // 1. Fetch the post content from WordPress
                const postResponse = await axios.get(`/wp-json/wp/v2/posts?slug=${postSlug}&_embed`);
                const post = postResponse.data[0];

                if (!post || !post.content.rendered) {
                    setError('Post not found or has no content.');
                    setLoading(false);
                    return;
                }

                // 2. Extract all anchor tags (links) from the content
                const parser = new DOMParser();
                const doc = parser.parseFromString(post.content.rendered, 'text/html');
                const links = Array.from(doc.querySelectorAll('a[href]'));

                const foundBrokenLinks = [];

                // 3. Check each link
                for (const link of links) {
                    const url = link.href;
                    // Basic check: ignore internal WordPress links or anchors
                    if (url.startsWith(window.location.origin) || url.startsWith('#')) {
                        continue;
                    }

                    try {
                        // Use a HEAD request for efficiency, fall back to GET if HEAD is not allowed
                        const response = await axios.head(url, {
                            timeout: 5000, // Set a timeout
                            headers: {
                                'User-Agent': 'WordPressBrokenLinkChecker/1.0' // Some servers might block requests without a User-Agent
                            }
                        });

                        if (response.status >= 400) {
                            foundBrokenLinks.push({ url, status: response.status });
                        }
                    } catch (linkError) {
                        // Handle network errors, timeouts, or 404s etc.
                        if (linkError.response) {
                            foundBrokenLinks.push({ url, status: linkError.response.status });
                        } else if (linkError.request) {
                            foundBrokenLinks.push({ url, status: 'Network Error' });
                        } else {
                            foundBrokenLinks.push({ url, status: 'Error' });
                        }
                    }
                }
                setBrokenLinks(foundBrokenLinks);

            } catch (err) {
                console.error('Error during link checking:', err);
                setError('Failed to fetch post or check links.');
            } finally {
                setLoading(false);
            }
        };

        checkLinks();
    }, [postSlug]); // Re-run effect if postSlug changes

    if (loading) {
        return <div>Checking links...</div>;
    }

    if (error) {
        return <div style={{ color: 'red' }}>Error: {error}</div>;
    }

    if (brokenLinks.length === 0) {
        return <div>No broken links found (or no links to check).</div>;
    }

    return (
        <div>
            <h3>Broken Links Found:</h3>
            <ul>
                {brokenLinks.map((link, index) => (
                    <li key={index}>
                        <a href={link.url} target="_blank" rel="noopener noreferrer">{link.url}</a> - Status: {link.status}
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default BrokenLinkChecker;

Now, integrate this component into your Next.js pages. For example, in pages/posts/[slug].js (assuming you have dynamic routing for posts):

import { useRouter } from 'next/router';
import { getPostBySlug, getPosts } from '../../lib/api'; // Adjust path as needed
import BrokenLinkChecker from '../../components/BrokenLinkChecker'; // Adjust path as needed

export default function PostPage({ post }) {
    const router = useRouter();

    // Find the block attributes from the post content.
    // This is a simplified approach. A more robust solution might involve
    // parsing the content for specific block structures or using a library.
    // For this example, we'll assume the block is added and its attributes
    // are somehow accessible or we can infer the slug.
    // A better approach for headless might be to store the slug in post meta.
    // For demonstration, let's assume we can get the slug from somewhere,
    // perhaps a custom field or by parsing the block's saved data if it were saved.

    // *** IMPORTANT NOTE FOR HEADLESS ***
    // Since the 'save' function returns null, the block's attributes (like postToCheck)
    // are NOT saved in the post_content. To make this work effectively in headless,
    // you would typically:
    // 1. Store the 'postToCheck' slug in a custom field (post meta) for the post
    //    where the block is *intended* to be used.
    // 2. Fetch this custom field data in your Next.js app.
    // 3. Pass the fetched slug to the BrokenLinkChecker component.

    // For this example, let's *simulate* having the slug.
    // In a real scenario, you'd fetch this from post meta.
    // const postToCheckSlug = post.meta.broken_link_post_slug; // Example if using ACF or similar

    // Placeholder: If you don't have post meta, you might hardcode it for testing
    // or derive it if the block itself was configured to reference its own post.
    // Let's assume for this example that the post slug itself is what we want to check.
    const postToCheckSlug = post?.slug; // This is a simplification.

    if (router.isFallback) {
        return <div>Loading...</div>;
    }

    return (
        <div>
            <h1>{post.title.rendered}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />

            {/* Render the BrokenLinkChecker component */}
            {postToCheckSlug && (
                <div style={{ marginTop: '40px', borderTop: '1px solid #eee', paddingTop: '20px' }}>
                    <BrokenLinkChecker postSlug={postToCheckSlug} />
                </div>
            )}
        </div>
    );
}

export async function getStaticPaths() {
    const posts = await getPosts();
    const paths = posts.map((post) => ({
        params: { slug: post.slug },
    }));

    return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
    const post = await getPostBySlug(params.slug);

    // Fetch post meta if you are using it for the slug
    // const postMeta = await getPostMeta(post.id); // Hypothetical function

    return {
        props: {
            post,
            // postMeta, // Pass meta if fetched
        },
        revalidate: 10, // Revalidate every 10 seconds
    };
}

Explanation of the Headless Integration:

  • `BrokenLinkChecker.js` Component: This React component takes a postSlug as a prop. It uses useEffect to fetch the specified post’s content from WordPress, parses the HTML to find all links, and then attempts to check the status of each link using axios.head (or axios.get as a fallback). It displays loading states, errors, and the list of broken links.
  • `pages/posts/[slug].js` (Dynamic Post Page): This Next.js page fetches a specific post using its slug. The critical part is how the postToCheckSlug is determined. In a true headless setup where the Gutenberg block’s save function returns null, the block’s attributes aren’t saved in the post content. Therefore, you need an alternative mechanism to pass the slug to the BrokenLinkChecker component. The most common and robust method is to use WordPress custom fields (post meta). You would add a custom field (e.g., “Post Slug to Check”) to the post type where you intend to use this block, populate it with the desired slug, and then fetch this meta data in getStaticProps. The example above simulates this by defaulting to the current post’s slug, which might be useful if you want to check links within the current page itself.
  • `getStaticProps` and `getStaticPaths`: These Next.js functions are used for static site generation, fetching data at build time. Ensure your API functions (`getPostBySlug`, `getPosts`) are correctly implemented in lib/api.js.

Refinements and Considerations

Error Handling and Robustness: The link checking logic can be expanded. Consider implementing retry mechanisms for network errors, handling different HTTP status codes more granularly, and potentially using a dedicated link-checking library if complexity increases.

Performance: Checking many links can be time-consuming. For production, consider:

  • Background Processing: Offload the link checking to a background job or a serverless function rather than performing it on every page request or build.
  • Caching: Cache the results of link checks to avoid redundant requests.
  • Rate Limiting: Be mindful of external servers’ rate limits. Implement delays between requests.

Security: Ensure your WordPress REST API is secured appropriately. For the link checker itself, be cautious about the URLs being checked. Malicious URLs could potentially exploit vulnerabilities in your frontend or backend if not handled carefully.

User Experience: Provide clear feedback to the user during the checking process. Displaying results in a user-friendly format is essential.

Alternative to `save: () => null`: While returning null is standard for headless, if you need to pass *some* data directly via the block’s saved content (e.g., for simpler blocks or fallback rendering), you could return a placeholder element with data attributes:

// In src/index.js, for the save function:
save: ({ attributes }) => {
    const { postToCheck } = attributes;
    // Return a div with data attributes that Next.js can read
    return (
        <div
            className="custom-block-placeholder"
            data-block="my-blocks/broken-link-checker"
            data-post-to-check={postToCheck}
        />
    );
},

Then, in your Next.js component, you would parse this placeholder element’s data attributes instead of relying solely on post meta. This can be more complex to manage reliably across different WordPress versions and block editor updates.

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

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

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 (48)
  • 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 (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

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