WordPress powers millions of websites, and its greatest strength is extensibility. When you need to add a new capability to your site, you usually start by searching for a ready-made plugin, but you cannot always find a solution that does exactly what you need. Sometimes existing plugins are too heavy, bloated with features you will never use, or simply do not fit your specific business logic. This is precisely the moment when writing your own plugin becomes the right path, because it does exactly what you require, stays lightweight, and remains entirely under your control.
Another important reason to write your own plugin is separating functionality from the theme. If you put logic directly into theme files, everything disappears the moment you switch themes. Code written inside a plugin works independently of the theme and survives even a complete redesign of the site. This is the professional approach and the one recommended by the WordPress community. Below we will walk through the entire process, from plugin structure to testing and distribution, supporting every step with practical code examples.
Plugin structure and the main file header
A WordPress plugin lives in the wp-content/plugins/ folder. The simplest plugin can be a single PHP file, but good practice is to create a dedicated folder for each plugin. For example, you create the folder wp-content/plugins/sayt-uz-tools/ and place the main file sayt-uz-tools.php inside it. For WordPress to recognise the plugin, you must write a special comment block at the top of this file, known as the plugin header. This header determines the information shown in the plugins list of the admin dashboard.
<?php
/**
* Plugin Name: Sayt UZ Tools
* Plugin URI: https://sayt.uz/plugins/sayt-uz-tools
* Description: Custom shortcodes and admin settings for your site.
* Version: 1.0.0
* Author: Sayt UZ
* Author URI: https://sayt.uz
* License: GPL-2.0+
* Text Domain: sayt-uz-tools
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
At minimum the header requires the Plugin Name field; the rest are optional but filling them in gives the plugin a professional look. The ABSPATH check at the bottom is very important: it blocks direct access to the file through a browser and ensures the code only runs through the WordPress core. This is the first step in security and should appear in every plugin file without exception.
The hook system: actions and filters
The heart of WordPress extensibility is the hook system. Hooks come in two types: actions and filters. An action lets you run your code at a specific point in the WordPress workflow, such as performing some task when a page loads or when a post is saved. A filter lets you modify data before it is sent to the screen, such as adding extra content to a post body before it is displayed. To understand the difference, remember a simple rule: an action does something, while a filter changes data and must always return a value.
// Action example: add text to the footer
function sayt_uz_footer_text() {
echo '<p style="text-align:center">Built by Sayt.uz</p>';
}
add_action( 'wp_footer', 'sayt_uz_footer_text' );
// Filter example: append a note to the post
function sayt_uz_append_notice( $content ) {
if ( is_single() ) {
$content .= '<p class="notice">Enjoyed the article? Share it!</p>';
}
return $content;
}
add_filter( 'the_content', 'sayt_uz_append_notice' );
In this example add_action tells WordPress "when the footer is rendered, call my function," while add_filter instructs "before showing the post body, pass it through my function." A filter function must always return the modified value; if you forget the return, the post content will come out empty. This is one of the most common mistakes, so whenever you work with filters, always remember the returned value.
A practical plugin: shortcode and settings page
Now let us build something genuinely useful. A shortcode is a mechanism that lets you insert a simple tag like [sayt_button] into a post or page, and full HTML is output in its place. This gives content editors the ability to add complex elements without writing any code. In the following example we create a shortcode that outputs a configurable button, where the text and link are passed as parameters.
function sayt_uz_button_shortcode( $atts ) {
$atts = shortcode_atts( array(
'text' => 'Contact',
'url' => '#',
), $atts, 'sayt_button' );
$text = esc_html( $atts['text'] );
$url = esc_url( $atts['url'] );
return '<a class="sayt-btn" href="' . $url . '">' . $text . '</a>';
}
add_shortcode( 'sayt_button', 'sayt_uz_button_shortcode' );
Now, if an editor writes [sayt_button text="Order" url="/order"] in any post, a neat button appears. Notice that we used the esc_html and esc_url functions; they sanitise the output data and protect against XSS attacks. Cleaning any data that comes from the user before printing it to the screen is the golden rule of WordPress security and must never be skipped.
Plugins often need a settings page. To add a new section to the admin menu we use the admin_menu hook, and to save the settings we apply a nonce together with data sanitisation.
function sayt_uz_admin_menu() {
add_options_page(
'Sayt UZ Tools',
'Sayt UZ Tools',
'manage_options',
'sayt-uz-tools',
'sayt_uz_settings_page'
);
}
add_action( 'admin_menu', 'sayt_uz_admin_menu' );
function sayt_uz_settings_page() {
if ( isset( $_POST['sayt_uz_save'] ) &&
check_admin_referer( 'sayt_uz_settings', 'sayt_uz_nonce' ) ) {
$val = sanitize_text_field( $_POST['sayt_uz_title'] );
update_option( 'sayt_uz_title', $val );
echo '<div class="updated"><p>Saved</p></div>';
}
$title = esc_attr( get_option( 'sayt_uz_title', '' ) );
echo '<div class="wrap"><h1>Settings</h1>';
echo '<form method="post">';
wp_nonce_field( 'sayt_uz_settings', 'sayt_uz_nonce' );
echo '<input type="text" name="sayt_uz_title" value="' . $title . '">';
echo '<input type="submit" name="sayt_uz_save" value="Save" class="button-primary">';
echo '</form></div>';
}
Best practices: prefixes and security
One of the most important rules when writing a plugin is to add a unique prefix to all function, variable and option names. In the examples above we used the sayt_uz_ prefix. The reason is that a WordPress site runs dozens of plugins at once, and if two plugins contain a function with the same name, the entire site will break. A prefix prevents such conflicts and keeps your code separated from other plugins, making it far more reliable.
Regarding security, always keep three principles in mind. First, sanitise any data coming from the user before saving it, using functions like sanitize_text_field. Second, escape it before printing to the screen with esc_html, esc_url or esc_attr. Third, use a nonce in every form, creating it with wp_nonce_field and verifying it with check_admin_referer. A nonce is a special token confirming that the request really came from your own site, protecting against CSRF attacks.
Activation, testing and distribution
When a plugin is activated and deactivated you may need to perform special actions, such as creating a database table or setting default options. The register_activation_hook and register_deactivation_hook functions are used for this. Once the plugin is written, activate it from the "Plugins" section of the WordPress admin dashboard and verify that no error messages appear. During testing, enable the WP_DEBUG mode so that all PHP warnings become visible, which helps you polish the code to perfection.
When your plugin is ready and reliable, you can distribute it. The simplest way is to pack the folder into a ZIP archive and install it through the "Upload Plugin" feature of the WordPress admin dashboard. If you want to offer the plugin to the wider public, you can submit it to the official WordPress.org directory, where the code is checked for GPL licence compliance and security requirements. The skill of writing your own plugins turns you from an ordinary WordPress user into a real developer and opens the door to limitless possibilities for your site.