If you want to know how to make a WordPress plugin, the short version is this: a plugin is a PHP file with a special comment header, placed in its own folder inside wp-content/plugins. WordPress reads the header, lists the plugin on the Plugins screen, and runs your code when it is activated. Everything else is about using hooks to add behavior at the right moment.
In this tutorial you will build a small but complete plugin that shows an estimated reading time on blog posts. It covers the plugin header, a direct access guard, filters, a shortcode, a setting on an existing admin screen, and clean uninstall. The code follows the WordPress coding standards, so you can use it as a starting template for your own plugins.
In short: create a folder in wp-content/plugins, add a PHP file with a Plugin Name header, block direct access, then hook your functions into WordPress with add_action and add_filter. Prefix every function, escape all output, sanitize all input, and clean up your data on uninstall.
How to make a WordPress plugin: what you need first
- A local WordPress site. Tools such as LocalWP, DevKinsta, wp-env or a simple PHP and MySQL stack all work. Do not develop on a live site.
- A code editor such as VS Code or PhpStorm.
- Basic PHP knowledge: functions, arrays and strings.
WP_DEBUGset totrueinwp-config.php, so notices and warnings show up while you work.
The official Plugin Handbook on WordPress.org is the reference for everything below, and worth bookmarking.
Step 1: Create the plugin folder and main file
Inside wp-content/plugins, create a folder named tnc-reading-time. Inside it, create tnc-reading-time.php. Using the same name for the folder, main file and text domain keeps things predictable, and it matches what the WordPress.org plugin directory expects.
The final structure is two files:
tnc-reading-time/
├── tnc-reading-time.php
└── uninstall.php
Step 2: Add the plugin header
The header comment is what turns a PHP file into a plugin. Only Plugin Name is required, but the other fields help users and are needed if you ever publish the plugin.
<?php
/**
* Plugin Name: TNC Reading Time
* Description: Shows an estimated reading time above single blog posts.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Your Name
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: tnc-reading-time
*/
Save the file and open Plugins in your dashboard. You will already see TNC Reading Time listed. You can activate it, although it does nothing yet.
Step 3: Block direct access
Plugin files should only run inside WordPress. If someone requests the file URL directly, WordPress is not loaded and the code could expose errors or behave unexpectedly. The standard guard checks for the ABSPATH constant, which WordPress defines when it boots:
defined( 'ABSPATH' ) || exit;
Put this line directly under the header in every PHP file of your plugin that is not meant to be loaded on its own.
Step 4: Write the core logic
Next, a helper function counts the words in a post and converts them to minutes. Notice the tncrt_ prefix. All plugins share one global PHP namespace, so a generic name like get_reading_time() could clash with another plugin and cause a fatal error.
/**
* Calculate the reading time of a post in minutes.
*
* @param int $post_id Post ID.
* @return int Minutes, at least 1.
*/
function tncrt_get_minutes( $post_id ) {
$content = get_post_field( 'post_content', $post_id );
$words = str_word_count( wp_strip_all_tags( strip_shortcodes( $content ) ) );
$wpm = absint( get_option( 'tncrt_words_per_minute', 200 ) );
if ( $wpm < 1 ) {
$wpm = 200;
}
return max( 1, (int) ceil( $words / $wpm ) );
}
/**
* Build the translated label, for example "Reading time: 4 minutes".
*
* @param int $post_id Post ID.
* @return string
*/
function tncrt_get_label( $post_id ) {
$minutes = tncrt_get_minutes( $post_id );
return sprintf(
/* translators: %d: number of minutes. */
_n( 'Reading time: %d minute', 'Reading time: %d minutes', $minutes, 'tnc-reading-time' ),
$minutes
);
}
_n() picks the singular or plural string and makes both translatable using the plugin’s text domain. The translators comment tells translators what the placeholder means.
Step 5: Hook into WordPress with a filter
Hooks are how plugins change WordPress without editing core files. There are two kinds:
| Hook type | Function | What it does | Used here for |
|---|---|---|---|
| Action | add_action() | Runs your code at a specific moment | Registering settings on admin_init |
| Filter | add_filter() | Receives a value, lets you change it, and must return it | Adding the label to the_content |
| Shortcode | add_shortcode() | Replaces a [tag] in content with your output | Placing the label anywhere |
| Activation hook | register_activation_hook() | Runs once when the plugin is activated | Saving a default setting |
The the_content filter passes the post content through your function before it is displayed. You add the label and return the result:
/**
* Prepend the reading time to single blog posts.
*
* @param string $content Post content.
* @return string
*/
function tncrt_prepend_label( $content ) {
if ( is_singular( 'post' ) && in_the_loop() && is_main_query() ) {
$label = sprintf(
'<p class="tncrt-reading-time">%s</p>',
esc_html( tncrt_get_label( get_the_ID() ) )
);
return $label . $content;
}
return $content;
}
add_filter( 'the_content', 'tncrt_prepend_label' );
The conditions matter. the_content also runs in widgets, excerpts and other loops, so checking is_singular( 'post' ), in_the_loop() and is_main_query() limits the output to the main post on single post pages. A filter must always return a value, otherwise the post content disappears.
esc_html() escapes the label before output. Escaping on output, as late as possible, is one of the core rules in the WordPress security guidelines.
Step 6: Add a shortcode
Some users will want the reading time in a different spot, such as a page builder header. A shortcode makes that possible:
/**
* Shortcode: [tnc_reading_time]
*
* @return string
*/
function tncrt_shortcode() {
$post_id = get_the_ID();
if ( $post_id ) {
return '<span class="tncrt-reading-time">' . esc_html( tncrt_get_label( $post_id ) ) . '</span>';
}
return '';
}
add_shortcode( 'tnc_reading_time', 'tncrt_shortcode' );
Shortcode callbacks must return their output, never echo it, or the text will appear in the wrong place on the page.
Step 7: Add a setting with sanitized input
Reading speed varies by audience, so let site owners change the words per minute. Instead of building a whole settings page, add one field to the existing Settings, Reading screen with the Settings API:
/**
* Register the words per minute setting on Settings > Reading.
*/
function tncrt_register_settings() {
register_setting(
'reading',
'tncrt_words_per_minute',
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 200,
)
);
add_settings_field(
'tncrt_words_per_minute',
__( 'Reading speed (words per minute)', 'tnc-reading-time' ),
'tncrt_render_field',
'reading',
'default',
array( 'label_for' => 'tncrt_words_per_minute' )
);
}
add_action( 'admin_init', 'tncrt_register_settings' );
/**
* Output the settings field.
*/
function tncrt_render_field() {
printf(
'<input type="number" min="50" max="1000" class="small-text" id="tncrt_words_per_minute" name="tncrt_words_per_minute" value="%s" />',
esc_attr( get_option( 'tncrt_words_per_minute', 200 ) )
);
}
sanitize_callback runs before the value is saved, so absint guarantees a whole, non negative number reaches the database. Because the field lives on a core screen, WordPress handles the nonce, the capability check and saving for you.
Step 8: Handle activation and uninstall
Save a default value when the plugin is activated. add_option() does nothing if the option already exists, so reactivating will not overwrite a user’s choice:
/**
* Set defaults on activation.
*/
function tncrt_activate() {
add_option( 'tncrt_words_per_minute', 200 );
}
register_activation_hook( __FILE__, 'tncrt_activate' );
When a user deletes the plugin, remove what it stored. Create uninstall.php in the plugin folder. WordPress runs it only on deletion and defines WP_UNINSTALL_PLUGIN first:
<?php
/**
* Uninstall TNC Reading Time.
*/
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
delete_option( 'tncrt_words_per_minute' );
Step 9: Test your plugin
- Activate the plugin and open a single blog post. The reading time should appear above the content.
- Check a page and the blog archive. The label should not appear there.
- Add
[tnc_reading_time]to a page or widget and confirm it renders. - Change the speed under Settings, Reading and confirm the minutes update.
- Enter a negative or text value and confirm it is sanitized.
- Watch for PHP notices with
WP_DEBUGenabled. - Deactivate, delete and confirm the option is gone from the
wp_optionstable.
For a deeper check, run the free Plugin Check plugin from WordPress.org, and PHP_CodeSniffer with the WordPress Coding Standards ruleset. Both catch escaping, prefixing and formatting issues early.
Best practices to keep as your plugin grows
- Prefix everything, or use a PHP namespace and classes once the plugin has more than a few functions.
- Sanitize input, validate data and escape output every time, with the right function for the context (
esc_html,esc_attr,esc_url). - Check capabilities and nonces on any custom form or AJAX handler.
- Load assets only where needed with
wp_enqueue_scriptandwp_enqueue_style, never hard coded tags. - Use the database sparingly. Options and post meta cover most needs before custom tables do.
When to hire a plugin developer
Knowing how to make a WordPress plugin like this one is a good foundation, and it makes a nice weekend project. Plugins that handle payments, user data, third party APIs, WooCommerce checkout logic or file access need more care with security, performance and long term maintenance. ThemeNcode has built and maintained WordPress plugins since 2014, and offers custom WordPress plugin development if you need something built properly from the start or want an existing plugin reviewed.
Frequently asked questions
How hard is it to make a WordPress plugin?
A basic plugin needs only a PHP file with a plugin header and a few hooks, so anyone with beginner PHP knowledge can build one in an afternoon. Complexity grows with features like settings pages, custom tables and integrations.
What language are WordPress plugins written in?
WordPress plugins are written mainly in PHP. Most also use JavaScript, CSS and HTML for the interface, and block editor plugins typically use React through the WordPress packages.
Where do I put my WordPress plugin files?
Create a folder for your plugin inside wp-content/plugins and put your main PHP file, with the plugin header, inside that folder.
Can I make a WordPress plugin without coding?
Some code snippet and plugin builder tools let you add small features without writing a full plugin, but anything custom or reusable still requires PHP. AI coding assistants can help, as long as you review the code for security.
How do I add my plugin to the WordPress.org directory?
Make sure it is GPL compatible, passes Plugin Check, and includes a readme.txt file, then submit it through the Add Your Plugin page on WordPress.org for manual review.