• 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 custom analytics tracker block for Gutenberg using Next.js headless configurations

Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Next.js headless configurations

Setting Up the Next.js Headless WordPress Environment

Before we can build our custom Gutenberg block, we need a robust headless WordPress setup. This involves configuring WordPress as a backend API and a Next.js application as the frontend. For this guide, we’ll assume you have a WordPress installation accessible via REST API and a Next.js project initialized. The key is ensuring your WordPress site is configured to serve JSON data for its content.

Ensure your WordPress REST API is enabled and accessible. By default, it is. You can test this by visiting your-wp-site.com/wp-json/wp/v2/posts in your browser. For the Next.js application, we’ll use a standard setup. The crucial part is how we’ll fetch data and integrate our custom block.

Developing the Custom Gutenberg Block with React and JavaScript

Gutenberg blocks are essentially React components. We’ll create a simple block that allows users to input a tracking ID and a platform name. This block will then render a script tag in the frontend to initialize an analytics service.

First, let’s set up the block’s directory structure within your WordPress theme or a custom plugin. A typical structure might look like this:

  • /your-plugin-or-theme/blocks/custom-analytics-tracker/
    • index.js (Main block registration file)
    • edit.js (Editor view component)
    • save.js (Frontend view component)
    • block.json (Block metadata)

block.json: Defining Block Metadata

This file describes your block to WordPress. It includes its name, title, category, and attributes (the data it will store).

{
  "apiVersion": 2,
  "name": "my-plugin/custom-analytics-tracker",
  "version": "0.1.0",
  "title": "Custom Analytics Tracker",
  "category": "widgets",
  "icon": "chart-pie",
  "description": "Adds a custom analytics tracker script to your content.",
  "attributes": {
    "trackingId": {
      "type": "string",
      "default": ""
    },
    "platform": {
      "type": "string",
      "default": "google-analytics"
    }
  },
  "editorScript": "file:./index.js",
  "editorStyle": "file:./editor.css",
  "style": "file:./style.css"
}

index.js: Block Registration

This file imports the necessary components and registers the block using registerBlockType.

import { registerBlockType } from '@wordpress/blocks';
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles

import Edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata.name, {
    edit: Edit,
    save,
} );

edit.js: The Editor Component

This component defines how the block appears and functions within the Gutenberg editor. We’ll use useBlockProps and InspectorControls for settings.

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

export default function Edit( { attributes, setAttributes } ) {
    const blockProps = useBlockProps();
    const { trackingId, platform } = attributes;

    const platforms = [
        { label: 'Google Analytics', value: 'google-analytics' },
        { label: 'Matomo', value: 'matomo' },
        { label: 'Custom Script', value: 'custom' },
    ];

    const onChangeTrackingId = ( newTrackingId ) => {
        setAttributes( { trackingId: newTrackingId } );
    };

    const onChangePlatform = ( newPlatform ) => {
        setAttributes( { platform: newPlatform } );
    };

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Analytics Settings', 'my-plugin' ) }>
                    <TextControl
                        label={ __( 'Tracking ID', 'my-plugin' ) }
                        value={ trackingId }
                        onChange={ onChangeTrackingId }
                        help={ __( 'Enter your analytics tracking ID (e.g., UA-XXXXX-Y or G-XXXXXXX).', 'my-plugin' ) }
                    />
                    <SelectControl
                        label={ __( 'Platform', 'my-plugin' ) }
                        value={ platform }
                        options={ platforms }
                        onChange={ onChangePlatform }
                    />
                </PanelBody>
            </InspectorControls>
            <div { ...blockProps }>
                <p>{ __( 'Analytics Tracker Block', 'my-plugin' ) }</p>
                { trackingId && (
                    <p>
                        { __( 'Tracking ID:', 'my-plugin' ) } { trackingId } <br />
                        { __( 'Platform:', 'my-plugin' ) } { platform }
                    </p>
                ) }
                { !trackingId && (
                    <p>{ __( 'Configure tracking ID and platform in the block settings.', 'my-plugin' ) }</p>
                ) }
            </div>
        </>
    );
}

save.js: The Frontend Component

This component defines the static HTML that will be saved to the database. For dynamic scripts, we’ll need to enqueue them separately or use a filter.

import { useBlockProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const blockProps = useBlockProps.save();
    const { trackingId, platform } = attributes;

    // Note: Actual script injection should ideally be handled via wp_enqueue_script
    // or a filter to avoid inline scripts where possible. For simplicity here,
    // we'll render a placeholder that can be processed.
    return (
        <div { ...blockProps } data-tracking-id={ trackingId } data-platform={ platform }>
            {/* This div will be used by a PHP hook to inject the script */}
            <!-- Analytics Tracker Placeholder -->
        </div>
    );
}

Enqueuing Scripts and Handling Dynamic Content in WordPress

Directly embedding scripts within the save.js component is generally discouraged for security and maintainability. A better approach is to use WordPress’s script enqueuing system and hooks. We’ll create a PHP function to conditionally enqueue our analytics script and inject the necessary tracking code.

In your plugin’s main PHP file (e.g., my-plugin.php), add the following:




assets/js/analytics-tracker.js: The Frontend Script

This JavaScript file will be enqueued and will handle the actual initialization of the analytics service based on the data passed from WordPress.

// assets/js/analytics-tracker.js

document.addEventListener( 'DOMContentLoaded', () => {
    // Check if our configuration exists
    if ( typeof window.analyticsTrackerConfig === 'undefined' ) {
        return;
    }

    // Iterate over all analytics tracker blocks on the page
    Object.keys( window.analyticsTrackerConfig ).forEach( ( blockId ) => {
        const config = window.analyticsTrackerConfig[ blockId ];
        const { trackingId, platform } = config;

        if ( ! trackingId ) {
            return; // Skip if no tracking ID
        }

        switch ( platform ) {
            case 'google-analytics':
                // Load Google Analytics (gtag.js)
                if ( ! document.getElementById( 'google-analytics-script' ) ) {
                    const script = document.createElement( 'script' );
                    script.id = 'google-analytics-script';
                    script.async = true;
                    script.src = `https://www.googletagmanager.com/gtag/js?id=${ trackingId }`;
                    document.head.appendChild( script );

                    script.onload = () => {
                        window.dataLayer = window.dataLayer || [];
                        function gtag(){ dataLayer.push(arguments); }
                        gtag( 'js', new Date() );
                        gtag( 'config', trackingId );
                        console.log( `Google Analytics initialized with ID: ${ trackingId }` );
                    };
                }
                break;

            case 'matomo':
                // Load Matomo
                // Assumes Matomo is self-hosted or configured correctly
                // This is a simplified example; Matomo integration can be complex.
                if ( ! document.getElementById( 'matomo-script' ) ) {
                    const script = document.createElement( 'script' );
                    script.id = 'matomo-script';
                    script.type = 'text/javascript';
                    script.async = true;
                    script.innerHTML = `
                        var _paq = window._paq = window._paq || [];
                        _paq.push(['trackPageView']);
                        _paq.push(['enableLinkTracking']);
                        (function() {
                            var u="//your-matomo-domain.com/"; // Replace with your Matomo URL
                            _paq.push(['setTrackerUrl', u+'matomo.php']);
                            _paq.push(['setSiteId', '${ trackingId }']); // Assuming trackingId is Site ID for Matomo
                            var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
                            g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
                        })();
                    `;
                    document.body.appendChild( script );
                    console.log( `Matomo initialized with Site ID: ${ trackingId }` );
                }
                break;

            case 'custom':
                // Handle custom script injection - this is highly dependent on the service
                // For demonstration, we'll just log it. In a real scenario, you'd fetch
                // and inject a specific script or configuration.
                console.log( `Custom analytics script for ID: ${ trackingId } would be loaded here.` );
                // Example:
                // const customScript = document.createElement('script');
                // customScript.src = `https://your-custom-analytics.com/script.js?id=${trackingId}`;
                // document.head.appendChild(customScript);
                break;
        }
    } );
} );

Integrating with Next.js Headless Configuration

In a headless setup, WordPress serves content via its REST API, and Next.js fetches and renders it. Our custom block's data (tracking ID, platform) will be part of the post content fetched from WordPress. When Next.js renders this content, it will receive the HTML generated by WordPress, including the placeholder div from our save.js component.

The critical part is how Next.js handles the dynamically enqueued WordPress scripts. Since Next.js typically renders on the server and then hydrates on the client, we need to ensure that the JavaScript intended to run client-side is correctly executed.

Fetching WordPress Content in Next.js

Use the fetch API or a library like axios to get post data from your WordPress REST API. For example, in a page component:

// pages/posts/[slug].js (Example)
import React from 'react';

export async function getServerSideProps( context ) {
    const { slug } = context.params;
    const res = await fetch( `https://your-wp-site.com/wp-json/wp/v2/posts?slug=${ slug }` );
    const posts = await res.json();
    const post = posts[0]; // Assuming a single post is returned

    if ( ! post ) {
        return {
            notFound: true,
        };
    }

    return {
        props: {
            post,
        },
    };
}

function PostPage( { post } ) {
    return (
        <div>
            <h1>{ post.title.rendered }</h1>
            {/* Render the HTML content from WordPress */}
            <div dangerouslySetInnerHTML={ { __html: post.content.rendered } } />
        </div>
    );
}

export default PostPage;

Handling WordPress-Generated Scripts in Next.js

When WordPress renders the post content, it includes the HTML output of our block. If our PHP enqueues scripts and adds inline data, Next.js's client-side hydration should pick this up. The dangerouslySetInnerHTML will render the HTML, and the browser will execute any inline scripts or load external scripts as instructed by the WordPress output.

The key is that the wp_enqueue_script and wp_add_inline_script calls happen on the server-side when WordPress generates the HTML. When this HTML is sent to the browser, the browser will process the script tags and inline JavaScript. Next.js's role here is primarily to render the HTML structure and manage the DOM.

Potential Challenges and Advanced Considerations

  • Script Loading Order: Ensure your analytics script loads after necessary DOM elements are available. The DOMContentLoaded event listener in analytics-tracker.js helps with this.
  • Server-Side Rendering (SSR) vs. Static Site Generation (SSG): If using SSG with Next.js, the analytics scripts won't run during the build process. They will only execute when the page is visited in the browser. For SSR, the scripts are generated with the HTML response.
  • Security: Be cautious with dangerouslySetInnerHTML. Ensure the content fetched from WordPress is sanitized if it comes from untrusted sources. WordPress's REST API usually handles basic sanitization, but review your setup.
  • Performance: Loading multiple analytics scripts can impact page load times. Consider lazy loading or conditional loading based on user interaction or consent.
  • Data Layer Management: For more complex analytics, you might need a more sophisticated way to manage the data layer (like window.dataLayer for Google Analytics) across different blocks and events.
  • WordPress Block Rendering in Next.js: Directly rendering Gutenberg blocks in a headless Next.js app can be complex. Libraries like @wordpress/block-serialization-default-author-styles or custom parsers might be needed for more advanced block types. For simple HTML-generating blocks, dangerouslySetInnerHTML is often sufficient.

By following these steps, you can create a custom Gutenberg block that integrates seamlessly with your Next.js headless WordPress setup, providing a flexible way to manage analytics tracking across your e-commerce site.

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 deep-seated hook priority conflicts in third-party Firebase Realtime DB connectors
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Alpine.js lightweight states
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Sage Roots modern environments
  • How to design secure Zapier dynamic webhooks webhook listeners using signature validation and payload queues
  • WordPress Development Recipe: Real-time custom event triggers using WebSockets and Metadata API (add_post_meta)

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 (41)
  • 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 (70)
  • WordPress Plugin Development (76)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging and Resolving deep-seated hook priority conflicts in third-party Firebase Realtime DB connectors
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Alpine.js lightweight states
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Sage Roots modern environments

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