Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Miami Will Let Rockstar Turn Downtown Into Vice City. The Fine Print Runs to Six Banners and a Deadline.

    September 24, 2026

    Apple Watch Series 12 Takes Aim at WHOOP and Oura With Always-On Heart Tracking

    September 24, 2026

    DoorDash Owes 264,000 Dashers $131.5 Million. Most of It Is an Argument About Waiting Around.

    September 24, 2026
    Facebook X (Twitter) Instagram Threads
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • Gaming
    • Smartwatch
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Make a WordPress Plugin That Does Something Useful
    Blog

    How to Make a WordPress Plugin That Does Something Useful

    Ethan CaldwellBy Ethan CaldwellSeptember 4, 202610 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    The fastest way to learn plugin development is to build one plugin all the way to the finish line instead of collecting isolated snippets. In this walkthrough you build GB Reading Time: a plugin that calculates an estimated reading time for a post and exposes it two ways, as a shortcode and as a real editor block defined by block.json. It ships with translations, an asset build, and a clean uninstall routine.

    Quick answer: Create a plugin folder with a header comment, write a helper that counts words with str_word_count( wp_strip_all_tags( $content ) ) and divides by a words per minute figure, register it with add_shortcode() for classic use, then add a dynamic block by placing a block.json with "apiVersion": 3 and a render file in a build directory and calling register_block_type( __DIR__ . '/build/reading-time' ) on init. Finish with uninstall.php so deleting the plugin removes its options.

    This is a complete project rather than a tour of concepts. If you have never touched a plugin header or a hook before, start with our beginner tutorial on creating a WordPress plugin, which explains folders, activation hooks, the Settings API, and escaping before you get here. Everything below assumes WordPress 6.7 or newer; the current release line is WordPress 7.1, and Node 20 or later for the build step.

    What we are building and why the two interfaces matter

    Reading time is a good first project because it touches every layer of a modern plugin without needing a database table. You need a pure PHP calculation, a way to render it inside post content, an editor experience, an asset pipeline, and a removal path. Offering both a shortcode and a block is not padding: shortcodes still work everywhere, including widgets, page builders, and templates, while a block is what people expect in the editor.

    ConcernShortcodeDynamic block
    Registrationadd_shortcode() on initregister_block_type() with a folder path
    Build stepNoneNode plus wp-scripts
    Editor previewRaw text onlyLive server rendered preview
    Settings UIAttributes typed by handSidebar controls
    Works in widgets and templatesYes, through do_shortcode()Yes in block themes

    Project layout

    Keep source and built assets separate. The src directory is what you edit, build is what wp-scripts produces and what WordPress actually registers.

    gb-reading-time/
    ├── gb-reading-time.php
    ├── uninstall.php
    ├── package.json
    ├── includes/
    │   └── class-gbrt-calculator.php
    ├── src/
    │   └── reading-time/
    │       ├── block.json
    │       ├── index.js
    │       ├── edit.js
    │       ├── style.scss
    │       └── render.php
    └── build/          # generated, do not edit

    Set up the toolchain with the official build package, which wraps webpack and Babel so you do not configure either.

    cd wp-content/plugins/gb-reading-time
    npm init -y
    npm install --save-dev @wordpress/scripts
    npx wp-scripts start     # watch mode while developing
    npx wp-scripts build     # minified output for release

    The main file and the calculation

    Start with the header and a small class that does one job. Keeping the math out of the render callbacks means the shortcode and the block share exactly one implementation.

    <?php
    /**
     * Plugin Name:       GB Reading Time
     * Description:       Estimated reading time as a shortcode and a block.
     * Version:           1.0.0
     * Requires at least: 6.7
     * Requires PHP:      7.4
     * Author:            Ethan Caldwell
     * License:           GPL-2.0-or-later
     * Text Domain:       gb-reading-time
     */
    
    if ( ! defined( 'ABSPATH' ) ) {
        exit;
    }
    
    require_once __DIR__ . '/includes/class-gbrt-calculator.php';

    Recommended for you:

    Why Is Jack the Black Cat Squishmallow So Rare?
    Blog·Sep 3, 2026

    Why Is Jack the Black Cat Squishmallow So Rare?

    <?php
    // includes/class-gbrt-calculator.php
    
    class GBRT_Calculator {
    
        const DEFAULT_WPM = 225;
    
        public static function minutes( $post_id = 0, $wpm = self::DEFAULT_WPM ) {
            $post = get_post( $post_id ?: get_the_ID() );
    
            if ( ! $post instanceof WP_Post ) {
                return 0;
            }
    
            $wpm  = max( 50, absint( $wpm ) );
            $text = wp_strip_all_tags( strip_shortcodes( $post->post_content ) );
            $n    = str_word_count( $text );
    
            return max( 1, (int) ceil( $n / $wpm ) );
        }
    
        public static function label( $minutes ) {
            return sprintf(
                /* translators: %s: number of minutes */
                _n( '%s min read', '%s min read', $minutes, 'gb-reading-time' ),
                number_format_i18n( $minutes )
            );
        }
    }

    Two decisions worth calling out. strip_shortcodes() runs before wp_strip_all_tags() so an embedded gallery does not inflate the count. And the words per minute value is clamped, because a zero passed in from an attribute would otherwise divide by zero.

    Note: str_word_count() is byte oriented and undercounts languages that do not separate words with spaces, such as Japanese. If your audience is multilingual, fall back to counting characters and dividing by a per language factor rather than pretending the English number is universal.

    Ship it as a shortcode first

    The shortcode is ten lines and gives you something testable before the build tooling is involved.

    add_action( 'init', 'gbrt_register_shortcode' );
    
    function gbrt_register_shortcode() {
        add_shortcode( 'reading_time', 'gbrt_shortcode' );
    }
    
    function gbrt_shortcode( $atts ) {
        $atts = shortcode_atts(
            array(
                'wpm'  => GBRT_Calculator::DEFAULT_WPM,
                'post' => 0,
            ),
            $atts,
            'reading_time'
        );
    
        $minutes = GBRT_Calculator::minutes( absint( $atts['post'] ), $atts['wpm'] );
    
        return '<span class="gbrt-time">' . esc_html( GBRT_Calculator::label( $minutes ) ) . '</span>';
    }

    Drop [reading_time wpm="200"] into any post and it renders. A shortcode callback must return a string, never echo, or the output jumps to the top of the page.

    Turn it into a block with block.json

    Since WordPress 5.8 the canonical way to define a block is metadata in block.json, and since 6.1 a dynamic block can point at a PHP render file directly. The current metadata version is apiVersion 3.

    {
      "$schema": "https://schemas.wp.org/trunk/block.json",
      "apiVersion": 3,
      "name": "gbrt/reading-time",
      "version": "1.0.0",
      "title": "Reading Time",
      "category": "widgets",
      "icon": "clock",
      "description": "Estimated reading time for the current post.",
      "textdomain": "gb-reading-time",
      "attributes": {
        "wpm": { "type": "number", "default": 225 }
      },
      "supports": {
        "html": false,
        "color": { "text": true, "background": false },
        "typography": { "fontSize": true }
      },
      "editorScript": "file:./index.js",
      "style": "file:./style-index.css",
      "render": "file:./render.php"
    }
    <?php
    // src/reading-time/render.php
    // $attributes, $content and $block are available here.
    
    $minutes = GBRT_Calculator::minutes( 0, $attributes['wpm'] ?? 225 );
    ?>
    <p <?php echo wp_kses_data( get_block_wrapper_attributes() ); ?>>
        <?php echo esc_html( GBRT_Calculator::label( $minutes ) ); ?>
    </p>

    Register it once, on init, pointing at the built folder. register_block_type() reads the metadata, enqueues the editor script and the stylesheet only on pages where the block appears, and wires the render file as the callback.

    add_action( 'init', 'gbrt_register_block' );
    
    function gbrt_register_block() {
        register_block_type( __DIR__ . '/build/reading-time' );
    }

    The editor side is a small React component. The important part is registering under the same name and reading metadata from the JSON file rather than repeating it.

    // src/reading-time/index.js
    import { registerBlockType } from '@wordpress/blocks';
    import metadata from './block.json';
    import Edit from './edit';
    import './style.scss';
    
    registerBlockType( metadata.name, { edit: Edit } );
    Tip: For a server rendered preview inside the editor, import ServerSideRender from @wordpress/server-side-render in edit.js. The editor then calls the REST endpoint and shows the same markup visitors get, so you never maintain two copies of the output.

    Translations, assets, and text domain

    Keep the text domain identical to the plugin slug. Plugins hosted on WordPress.org have their PHP translations loaded automatically, but JavaScript strings need one extra call so the editor picks up the JSON translation files.

    add_action( 'init', 'gbrt_set_translations', 20 );
    
    function gbrt_set_translations() {
        wp_set_script_translations(
            'gbrt-reading-time-editor-script',
            'gb-reading-time',
            plugin_dir_path( __FILE__ ) . 'languages'
        );
    }

    Generate the POT file with WP-CLI using wp i18n make-pot . languages/gb-reading-time.pot, and convert PO files for the editor with wp i18n make-json languages/. The handle passed to wp_set_script_translations() is generated from the block name, so check it against the output of wp_scripts() if translations do not appear.

    uninstall.php so removal is clean

    Deactivation should leave data alone. Deletion should not. Put an uninstall.php at the plugin root and WordPress runs it when someone deletes the plugin from the Plugins screen.

    <?php
    // uninstall.php
    
    if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
        exit;
    }
    
    delete_option( 'gbrt_default_wpm' );
    delete_site_option( 'gbrt_default_wpm' );
    
    delete_metadata( 'post', 0, '_gbrt_cached_minutes', '', true );
    Warning: The WP_UNINSTALL_PLUGIN guard is not optional. Without it the file can be requested directly and will delete data on demand. Also remember that uninstall.php takes precedence over any uninstall hook you registered, so do not maintain both.

    Troubleshooting

    The block does not appear in the inserter. Nine times out of ten the build folder is missing or stale. Run npx wp-scripts build and confirm build/reading-time/block.json exists. wp-scripts copies block.json and render.php into the build output automatically, so registering against src is the wrong path.

    The editor shows “Your site does not include support for this block”. The block name in block.json and the name used in registerBlockType() disagree, or the block is registered on the server but the editor script failed to load. Open the browser console and look for a 404 on index.js.

    The shortcode prints the raw text instead of rendering. Something is calling the_content without shortcode processing, or the shortcode was registered too late. Registering inside an init callback rather than at file scope fixes the ordering.

    Reading time is always one minute. The calculator is receiving an empty post_content, which happens when the block renders in a template context with no queried post. Pass an explicit post ID, or bail out when get_the_ID() returns false.

    Recommended for you:

    Blog·Sep 4, 2026

    How to Interpret the Ichimoku Cloud in Trading

    Styles do not load on the front end. The style key in block.json must point at the compiled file name, which wp-scripts emits as style-index.css, not style.scss.

    Frequently asked questions

    Do I have to use Node just to make a block?

    Not for a purely dynamic block. You can register a block with only a block.json and a PHP render file, skipping the editor script entirely, and the block appears with a generic placeholder in the editor. You need Node the moment you want custom sidebar controls or a rich editor preview.

    Should I use create-block instead of building the folder by hand?

    Running npx @wordpress/create-block is a reasonable shortcut and produces the same structure shown here. Doing it manually once is worth the hour because you learn which file does what, which makes debugging a scaffolded project far less mysterious later.

    How do I cache the reading time instead of recalculating it?

    Store the result in post meta on the save_post hook and read the meta in the render callback, falling back to a live calculation when the meta is missing. For a word count on a normal post the calculation is cheap, so only bother if you are rendering hundreds of items in a loop.

    Can one plugin register several blocks?

    Yes. Give each block its own folder under src, and either call register_block_type() once per folder or use wp_register_block_types_from_metadata_collection() on newer releases. Keeping one block per directory keeps the build output predictable and lets WordPress load only the assets a page needs.

    What is the next step after this build?

    Either package it for distribution or restructure it for scale. Submitting a plugin to the WordPress repository covers readme.txt and the Subversion workflow, while building a WordPress plugin from scratch replaces the include statements here with Composer autoloading and tests.

    Wrapping up

    You now have a plugin with a shared calculation, two rendering surfaces, a real asset build, translation support, and an uninstall path. That combination is what separates a snippet from something you can hand to another person without an apology attached.

    Test it against a long post and a nearly empty one, delete it and confirm the options are gone, then read the official block metadata reference for the attributes and supports you did not use here. If you are storing anything larger later, our piece on where WordPress plugin settings are stored explains when options stop being the right home.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleHow Keltner Channels Are Calculated and Used
    Next Article How to Interpret the Ichimoku Cloud in Trading
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem - iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      10 Mins Read

      Google Ads Keyword Planner: How It Shows the Most Relevant Keywords

      11 Mins Read

      How to Use Remarketing Techniques for Better Conversions

      11 Mins Read

      How to Conduct A/B Testing for Marketing Campaigns

      10 Mins Read

      AMP for WP Plugin Vulnerability: What Was Fixed and What to Do

      11 Mins Read

      How to Create a Facebook Business Page in 2026

      11 Mins Read

      How to Convert GMT Time to Other Time Zones in C++

      Top Posts

      How to Fix PS5 Controller Stick Drift (2026): 7 Working Methods

      July 10, 20262 Views

      Best Free Online Music Apps in 2026

      July 7, 20262 Views

      COD Mobile Best Loadouts and Meta Guns (2026 Guide)

      July 2, 20262 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      How to Convert HEIC to JPG on iPhone, Mac, Android and Windows

      September 3, 20266 Views

      Gal Gadot’s Lawyers Spent Six Months on One AI Clause. Then SAG Called Them for Pointers.

      September 2, 20265 Views

      The Mesh Router Placement Strategy That Finally Gave Me Full Home Coverage

      September 9, 20263 Views
      Our Picks

      Miami Will Let Rockstar Turn Downtown Into Vice City. The Fine Print Runs to Six Banners and a Deadline.

      September 24, 2026

      Apple Watch Series 12 Takes Aim at WHOOP and Oura With Always-On Heart Tracking

      September 24, 2026

      DoorDash Owes 264,000 Dashers $131.5 Million. Most of It Is an Argument About Waiting Around.

      September 24, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      HEICJPG.online - Convert HEIC to JPG online
      Facebook
      • About Us
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      • Editorial Policy
      • Cookie Policy
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.