Introduction to WordPress Plugin Development: Build Your First Plugin

Belinda AllenDyn365GP23 hours ago28 Views

WordPress powers over 40% of the web, and much of its flexibility comes from plugins. Plugins are self-contained bundles of PHP, JavaScript, and other assets that extend what WordPress can do—powering everything from simple tweaks to complex business features. If you’re a developer new to WordPress, learning how to build plugins is the gateway to customizing and scaling the platform for any need.

In this guide, you’ll learn the essentials of plugin development, set up a local environment using WordPress Studio, and build a fully functional example plugin. By the end, you’ll understand the anatomy of a plugin, how hooks work, and best practices for a maintainable and secure code.

Table of Contents

Setting up a local development environment

Before you write a single line of code, you need a local WordPress environment. WordPress Studio is the fastest way to get started. Studio is open source, maintained by Automattic, and designed for seamless WordPress development.

Follow these steps:

Step 1: Download and install Studio

Visit developer.wordpress.com/studio and download the installer for macOS or Windows.

Step 2: Create your first local site

To create a local site, launch Studio and click Add Site. You’ll see a simple window where you can name your new site. After entering a name and clicking Add Site, Studio automatically configures a complete WordPress environment for you—no command line knowledge needed. Once complete, your new site appears in Studio’s sidebar, providing convenient links to view it in your browser or access the WordPress admin dashboard.

Add Site to WordPress Studio.

Step 3: Open your WordPress site and its admin section

Click the “Open site” link to open your site in the browser. You can also click the “WP Admin” button in Studio to access your site’s dashboard at /wp-admin. You’ll be automatically logged in as an Administrator. This is where you’ll manage plugins, test functionality, and configure settings.

Click Open Site to view site in browser.

Step 4: Open the code in your IDE

Studio provides convenient “Open in…” buttons that detect your installed code editor (like Visual Code or Cursor) and let you open your project in your preferred editor. You can configure your default code editor in Studio’s settings. Once opened in your code editor, you’ll have complete access to browse, edit, and debug the WordPress installation files.

Once you have your local environment for WordPress development set up and running, locate the plugins folder . In your project root, navigate to:

wp-content/
  └── plugins/

This is where all plugins live. To build your own, create a new folder (e.g., quick-reading-time) and add your plugin files there. Studio’s server instantly reflects changes when you reload your local site.

Example of a folder structure, with a folder labelled Quick Reading Time.

Creating your first plugin

Every plugin starts as a folder with at least one PHP file. Let’s build a minimal “Hello World” plugin to demystify the process.

  1. In wp-content/plugins/, create a folder called quick-reading-time.
  2. Inside that folder, create a file named quick-reading-time.php.

Your file structure should look like this:

wp-content/
  └── plugins/
    └── quick-reading-time/
      └── quick-reading-time.php

Add the following code to quick-reading-time.php:

<?php
/*
Plugin Name: Quick Reading Time
Description: Displays an estimated reading-time badge beneath post titles.
Version:     1.0
Author:      Your Name
License:     GPL-2.0+
Text Domain: quick-reading-time
*/

This header is a PHP comment, but WordPress scans it to list your plugin in Plugins → Installed Plugins. Activate it—nothing happens yet (that’s good; nothing is broken).

Tip: Each header field has a purpose. For example, Text Domain enables translation, and License is required for distribution in the Plugin Directory. Learn more in the Plugin Developer Handbook.

Understanding hooks: actions and filters

WordPress plugins interact with core events using hooks. There are two types:

  • Actions: Triggered when WordPress does something (e.g., loading scripts, saving posts).
  • Filters: Allow you to modify data before it’s displayed or saved.

Let’s add a reading-time badge using the the_content filter:

function qrt_add_reading_time( $content ) {
    // Only on single posts in the main loop
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }

    // 1. Strip HTML/shortcodes, count words
    $plain   = wp_strip_all_tags( strip_shortcodes( get_post()->post_content ) );
    $words   = str_word_count( $plain );

    // 2. Estimate: 200 words per minute
    $minutes = max( 1, ceil( $words / 200 ) );

    // 3. Build the badge
    $badge = sprintf(
        '<p class="qrt-badge" aria-label="%s"><span>%s</span></p>',
        esc_attr__( 'Estimated reading time', 'quick-reading-time' ),
        /* translators: %s = minutes */
        esc_html( sprintf( _n( '%s min read', '%s mins read', $minutes, 'quick-reading-time' ), $minutes ) )
    );

    return $badge . $content;
}
add_filter( 'the_content', 'qrt_add_reading_time' );

This snippet adds a reading time badge to post content using the the_content filter. It checks context with is_singular(), in_the_loop(), and is_main_query() to ensure the badge only appears on single posts in the main loop.

The code strips HTML and shortcodes using wp_strip_all_tags() and strip_shortcodes(), counts words, and estimates reading time. Output is localized with esc_attr__() and _n(). The function is registered with add_filter().

With this plugin activated, each post will now also display the reading time:

Loading assets the WordPress way

To style your badge, enqueue a stylesheet using the wp_enqueue_scripts action:

function qrt_enqueue_assets() {
    wp_enqueue_style(
        'qrt-style',
        plugin_dir_url( __FILE__ ) . 'style.css',
        array(),
        '1.0'
    );
}
add_action( 'wp_enqueue_scripts', 'qrt_enqueue_assets' );

Create a style.css file in the same folder:

.qrt-badge span {
    margin: 0 0 1rem;
    padding: 0.25rem 0.5rem;
    display: inline-block;
    background: #f5f5f5;
    color: #555;
    font-size: 0.85em;
    border-radius: 4px;
}

Best practice: Only load assets when needed (e.g., on the front end or specific post types) for better performance.

With this change, the reading time info on each post should look like this:

Reading time info displayed on a blog post.

Optional: Adding a settings screen

To make the average reading speed configurable, let’s add a settings page and connect it to our plugin logic. We’ll store the user’s preferred words-per-minute (WPM) value in the WordPress options table and use it in our reading time calculation.

Step 1: Register the setting

Add this code to your plugin file to register a new option and settings field:

// Register the setting during admin_init.
function qrt_register_settings() {
    register_setting( 'qrt_settings_group', 'qrt_wpm', array(
        'type' => 'integer',
        'sanitize_callback' => 'qrt_sanitize_wpm',
        'default' => 200,
    ) );
}
add_action( 'admin_init', 'qrt_register_settings' );

// Sanitize the WPM value.
function qrt_sanitize_wpm( $value ) {
    $value = absint( $value );
    return ( $value > 0 ) ? $value : 200;
}

This code registers a plugin option (qrt_wpm) for words-per-minute, using register_setting() on the admin_init hook. The value is sanitized with a custom callback using absint() to ensure it’s a positive integer.

Step 2: Add the settings page

Add a new page under Settings in the WordPress admin:

function qrt_register_settings_page() {
    add_options_page(
        'Quick Reading Time',
        'Quick Reading Time',
        'manage_options',
        'qrt-settings',
        'qrt_render_settings_page'
    );
}
add_action( 'admin_menu', 'qrt_register_settings_page' );

This code adds a settings page for your plugin under the WordPress admin “Settings” menu. It uses add_options_page() to register the page, and hooks the function to admin_menu so it appears in the dashboard. The callback (qrt_render_settings_page) will output the page’s content.

Step 3: Render the settings page

Display a form for the WPM value and save it using the Settings API:

function qrt_render_settings_page() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    ?>
    <div class="wrap">
        <h1><?php esc_html_e( 'Quick Reading Time Settings', 'quick-reading-time' ); ?></h1>
        <form method="post" action="options.php">
            <?php
            settings_fields( 'qrt_settings_group' );
            do_settings_sections( 'qrt_settings_group' );
            $wpm = get_option( 'qrt_wpm', 200 );
            ?>
            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row">
                        <label for="qrt_wpm"><?php esc_html_e( 'Words Per Minute', 'quick-reading-time' ); ?></label>
                    </th>
                    <td>
                        <input name="qrt_wpm" type="number" id="qrt_wpm" value="<?php echo esc_attr( $wpm ); ?>" class="small-text" min="1" />
                        <p class="description"><?php esc_html_e( 'Average reading speed for your audience.', 'quick-reading-time' ); ?></p>
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

This function renders the plugin’s settings page, displaying a form to update the WPM value. It checks user permissions with current_user_can(), outputs the form using settings_fields(), do_settings_sections(), and retrieves the saved value with get_option(). The form submits to the WordPress options system for secure saving.

Step 4: Use the setting in your plugin logic

Update your reading time calculation to use the saved WPM value:

function qrt_add_reading_time( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }
    $plain   = wp_strip_all_tags( strip_shortcodes( get_post()->post_content ) );
    $words   = str_word_count( $plain );
    $wpm     = (int) get_option( 'qrt_wpm', 200 );
    $minutes = max( 1, ceil( $words / $wpm ) );
    $badge = sprintf(
        '<p class="qrt-badge" aria-label="%s"><span>%s</span></p>',
        esc_attr__( 'Estimated reading time', 'quick-reading-time' ),
        esc_html( sprintf( _n( '%s min read', '%s mins read', $minutes, 'quick-reading-time' ), $minutes ) )
    );
    return $badge . $content;
}

This function adds a reading time badge to post content. It checks context with is_singular(), in_the_loop(), and is_main_query() to ensure it runs only on single posts in the main loop. It strips HTML and shortcodes using wp_strip_all_tags() and strip_shortcodes()), counts words, and retrieves the WPM value with get_option(). The badge is output with proper escaping and localization using esc_attr__(), esc_html(), and _n()).

With these changes, your plugin now provides a user-friendly settings page under Settings → Quick Reading Time. Site administrators can set the average reading speed for their audience, and your plugin will use this value to calculate and display the estimated reading time for each post.

Complete plugin code

Before we wrap up with best practices, let’s review the complete code for the “Quick Reading Time” plugin you built in this guide. This section brings together all the concepts covered—plugin headers, hooks, asset loading, and settings—into a single, cohesive example. Reviewing the full code helps solidify your understanding and provides a reference for your own projects.

At this stage, you should have a folder named quick-reading-time inside your wp-content/plugins/ directory, and a file called quick-reading-time.php with the following content:

<?php
/*
Plugin Name: Quick Reading Time
Description: Displays an estimated reading-time badge beneath post titles.
Version:     1.0
Author:      Your Name
License:     GPL-2.0+
Text Domain: quick-reading-time
*/

// Register the WPM setting during admin_init.
function qrt_register_settings() {
    register_setting( 'qrt_settings_group', 'qrt_wpm', array(
        'type' => 'integer',
        'sanitize_callback' => 'qrt_sanitize_wpm',
        'default' => 200,
    ) );
}
add_action( 'admin_init', 'qrt_register_settings' );

// Sanitize the WPM value.
function qrt_sanitize_wpm( $value ) {
    $value = absint( $value );
    return ( $value > 0 ) ? $value : 200;
}

// Add a settings page under Settings.
function qrt_register_settings_page() {
    add_options_page(
        'Quick Reading Time',
        'Quick Reading Time',
        'manage_options',
        'qrt-settings',
        'qrt_render_settings_page'
    );
}
add_action( 'admin_menu', 'qrt_register_settings_page' );

// Render the settings page.
function qrt_render_settings_page() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    ?>
    <div class="wrap">
        <h1><?php esc_html_e( 'Quick Reading Time Settings', 'quick-reading-time' ); ?></h1>
        <form method="post" action="options.php">
            <?php
            settings_fields( 'qrt_settings_group' );
            do_settings_sections( 'qrt_settings_group' );
            $wpm = get_option( 'qrt_wpm', 200 );
            ?>
            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row">
                        <label for="qrt_wpm"><?php esc_html_e( 'Words Per Minute', 'quick-reading-time' ); ?></label>
                    </th>
                    <td>
                        <input name="qrt_wpm" type="number" id="qrt_wpm" value="<?php echo esc_attr( $wpm ); ?>" class="small-text" min="1" />
                        <p class="description"><?php esc_html_e( 'Average reading speed for your audience.', 'quick-reading-time' ); ?></p>
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

// Add the reading time badge to post content.
function qrt_add_reading_time( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }
    $plain   = wp_strip_all_tags( strip_shortcodes( get_post()->post_content ) );
    $words   = str_word_count( $plain );
    $wpm     = (int) get_option( 'qrt_wpm', 200 );
    $minutes = max( 1, ceil( $words / $wpm ) );
    $badge = sprintf(
        '<p class="qrt-badge" aria-label="%s"><span>%s</span></p>',
        esc_attr__( 'Estimated reading time', 'quick-reading-time' ),
        esc_html( sprintf( _n( '%s min read', '%s mins read', $minutes, 'quick-reading-time' ), $minutes ) )
    );
    return $badge . $content;
}
add_filter( 'the_content', 'qrt_add_reading_time' );

// Enqueue the plugin stylesheet.
function qrt_enqueue_assets() {
    wp_enqueue_style(
        'qrt-style',
        plugin_dir_url( __FILE__ ) . 'style.css',
        array(),
        '1.0'
    );
}
add_action( 'wp_enqueue_scripts', 'qrt_enqueue_assets' );

You should also have a style.css file in the same folder with the following content to style the badge:

.qrt-badge span {
    margin: 0 0 1rem;
    padding: 0.25rem 0.5rem;
    display: inline-block;
    background: #f5f5f5;
    color: #555;
    font-size: 0.85em;
    border-radius: 4px;
}

This plugin demonstrates several foundational concepts in WordPress development:

  • Plugin Header: The block comment at the top registers your plugin with WordPress, making it discoverable and manageable from the admin dashboard.
  • Hooks: The plugin uses both actions (admin_init, admin_menu, wp_enqueue_scripts) and a filter (the_content) to integrate with WordPress at the right moments.
  • Settings API: By registering a custom option and rendering a settings page, the plugin allows site administrators to configure the average reading speed, making the feature flexible and user-friendly.
  • Sanitization and Security: All user input is sanitized, and output is escaped, following best practices to prevent security vulnerabilities.
  • Asset Loading: Styles are loaded using WordPress’s enqueue system, ensuring compatibility and performance.
  • Internationalization: All user-facing strings are wrapped in translation functions, making the plugin ready for localization.

By bringing these elements together, you have a robust, maintainable, and extensible plugin foundation. Use this as a template for your own ideas, and continue exploring the WordPress Plugin Developer Handbook for deeper knowledge.

Best practices for plugin development

Building a WordPress plugin is more than just making something work—it’s about creating code that is robust, secure, and maintainable for years to come. As your plugin grows or is shared with others, following best practices becomes essential to avoid pitfalls that can lead to bugs, security vulnerabilities, or compatibility issues. The habits you form early in your development journey will shape the quality and reputation of your work.

Let’s explore the foundational principles that set apart professional WordPress plugin development.

  • Prefix everything (e.g., qrt_) to avoid name collisions. WordPress is a global namespace, so unique prefixes for functions, classes, and even option names help prevent conflicts with other plugins or themes.
  • Escape and sanitize all output and input to prevent XSS and security issues. Always validate and clean data before saving it to the database or displaying it in the browser. Use functions like esc_html(), esc_attr(), and sanitize_text_field() to keep your plugin safe.
  • Translate strings using __(), and _n() for localization. Internationalization (i18n) ensures your plugin is accessible to users worldwide. Wrap all user-facing text in translation functions and provide a text domain.
  • Use version control (Git) and WP-CLI helpers (wp scaffold plugin, wp i18n make-pot). Version control is your safety net, allowing you to track changes, collaborate, and roll back mistakes. WP-CLI tools can automate repetitive tasks and enforce consistency.
  • Ship a readme.txt for the Plugin Directory and changelog. A well-written readme helps users understand your plugin’s features, installation steps, and update history. It’s also required for distribution on WordPress.org.
  • Debugging: Enable WP_DEBUG and use tools like Query Monitor for troubleshooting. Proactive debugging surfaces issues early, making them easier to fix and improving your plugin’s reliability.
  • Follow the Plugin Developer Handbook and WordPress Coding Standards. These resources are the gold standard for WordPress development, offering guidance on everything from code style to security.

Tip: Adopt these habits early—retrofitting best practices later is much harder. By making them part of your workflow from the start, you’ll save time, reduce stress, and build plugins you can be proud of.

Next steps and resources

You now have a working plugin that demonstrates the three “golden” hooks:

Where you go next is up to you—try adding custom post types (init), REST API endpoints (rest_api_init), scheduled events, or Gutenberg blocks (register_block_type). The mental model is the same: find the hook, write a callback, let WordPress run it.

Your plugin journey starts here

Every plugin—whether 40 KB or 40 MB—starts with a folder, a header, and a hook. Master that foundation, and the rest of the WordPress ecosystem opens wide. Experiment locally, keep your code readable and secure, and iterate in small steps. With practice, the leap from “I wish WordPress could…” to “WordPress does” becomes second nature.

Ready to build your own plugin? Try the steps above, share your results in the comments, or explore more advanced topics in our developer blog. Happy coding!

Original Post https://wordpress.com/blog/2025/07/31/introduction-to-wordpress-plugin-development/

0 Votes: 0 Upvotes, 0 Downvotes (0 Points)

Leave a reply

Follow
Search
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...