©2021 Reporters Post24. All Rights Reserved.
WordPress powers over 40% of the web, and much of its flexibility comes from plugin. 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
- Creating your first plugin
- Understanding hooks: actions and filters
- Loading assets the WordPress way
- Optional: Adding a settings screen
- Complete plugin code
- Best practices for plugin development
- Next steps and resources
- Your plugin journey starts here
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.

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.

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.

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.
- In
wp-content/plugins/
, create a folder calledquick-reading-time
. - 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 1 rem; padding : 0.25 rem 0.5 rem; 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:

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 1 rem; padding : 0.25 rem 0.5 rem; 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()
, andsanitize_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:
the_content
– injects the badge.wp_enqueue_scripts
– loads the stylesheet.admin_menu
– (optionally) adds a settings page.
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.