Your own plugin
A plugin is code that adds features to a site and does not depend on the theme. A custom post type, a form, a link to an external service must keep working after a change of design, so they are written in a plugin rather than in functions.php.
The plugin file
Create the wp-content/plugins/reading-time folder and in it a file of the same name:
<?php
/**
* Plugin Name: Reading Time
* Description: Shows the approximate reading time of a post.
* Version: 1.0.0
* Author: Anna Smith
* Text Domain: reading-time
*/
// reading-time.php
defined('ABSPATH') || exit;
add_filter('the_content', function (string $content): string {
if (!is_singular('post')) {
return $content;
}
$minutes = max(1, (int) ceil(str_word_count(wp_strip_all_tags($content)) / 200));
return '<p class="reading-time">Reading time: ' . $minutes . ' min</p>' . $content;
});The plugin appears under "Plugins". Activate it — the reading time shows up before the text of every post.
Guarding against direct access
The defined('ABSPATH') || exit; line stops the file when it is opened directly by address rather than through WordPress. It goes at the top of every plugin file.
Activation and deactivation
// reading-time.php
register_activation_hook(__FILE__, function () {
add_option('reading_time_speed', 200);
});
register_deactivation_hook(__FILE__, function () {
// stop scheduled tasks
});Activation runs once when the plugin is turned on — tables and default settings are created here.
Uninstalling
When a plugin is deleted from the dashboard, WordPress runs the uninstall.php file. It cleans up:
<?php
// uninstall.php
defined('WP_UNINSTALL_PLUGIN') || exit;
delete_option('reading_time_speed');Paths and addresses
// reading-time.php
require_once plugin_dir_path(__FILE__) . 'includes/settings.php';
add_action('wp_enqueue_scripts', function () {
wp_enqueue_style('reading-time', plugins_url('assets/style.css', __FILE__), [], '1.0.0');
});plugin_dir_path() returns the folder path on disk, plugins_url() the file address for the browser.
Unique names
All plugins and the theme run in one PHP namespace. A function named get_settings() will almost certainly collide with someone else's. There are two ways to avoid that — a prefix or a namespace:
// reading-time.php
namespace ReadingTime;
function minutes(string $content): int
{
return max(1, (int) ceil(str_word_count(wp_strip_all_tags($content)) / 200));
}
add_filter('the_content', __NAMESPACE__ . '\\prepend');
function prepend(string $content): string
{
return '<p>' . minutes($content) . ' min</p>' . $content;
}A prefix is added to the names of options, hooks and tables as well: reading_time_speed, not speed.
Must-use plugins
A file placed in the wp-content/mu-plugins folder is always active and cannot be turned off from the dashboard. Code the site must not run without is added this way.
Common mistake
Activation code runs only when the plugin is turned on. If you add table creation to it after the plugin is already active, the table does not appear. During development deactivate the plugin and activate it again, and for sites that already have the plugin keep a version number in the options and update the structure when it changes.