Learn to create a WordPress plugin is the step that separates those who use the platform from those who truly master it. A plugin is nothing more than a set of PHP files that WordPress loads alongside the core and can modify or extend almost any system behavior: add functions, change texts, create content types, or connect the website with external services. The good news is that the entry barrier is much lower than it seems: with one file, a comment header, and basic knowledge of hooks, you already have a functional plugin. In this guide, we will build one from scratch, step by step and with complete code that you can copy, test, and extend. We will cover the recommended file structure, how actions and filters work, how to add a settings page, and what good security practices you should apply from day one to make your code robust and maintainable.
Why create a WordPress plugin and not touch the theme
Before writing a single line, it's good to understand where each type of code should live in WordPress. The general rule is simple: the theme controls how your website looks, and plugins control what your website does. If you add functionality to the file functions.php of your theme, that functionality will disappear as soon as you change themes or, even worse, as soon as the theme updates and overwrites your changes.
A plugin, on the other hand, is independent of the design. You can activate it, deactivate it, move it to another installation, or publish it in the official directory. These are the typical situations where the correct answer is create a WordPress plugin custom:
- You need a specific function (a shortcode, a notice, an integration) that doesn't justify installing a huge commercial plugin.
- You want the functionality to survive a theme change.
- You are going to reuse the same code on several client websites.
- An existing plugin does almost what you want, but not exactly, and you prefer to control the code.
For very small snippets, there's an intermediate alternative: snippet managers. If you only need to paste ten lines of PHP, you might be interested in reading our analysis of WPCode, the WordPress snippet manager. But as soon as the code grows or needs organization, a custom plugin wins by a landslide.
What you need before starting
The toolkit is minimal. No paid software or complicated environment is needed:
- A test WordPress installation. Never develop directly in production. A local environment with Local WP, XAMPP, or similar is ideal.
- A code editor. Visual Studio Code is the most popular and free option.
- File access. Locally you have it direct; on a server, via SFTP or the hosting's file manager.
- Basic PHP concepts. Variables, functions, and arrays are enough to start.
It's also advisable to activate debug mode while developing. Add this to your wp-config.php of the testing environment:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
With this configuration, errors and notices are saved in wp-content/debug.log instead of being displayed on screen, which will allow you to catch problems without breaking the browsing experience.
The minimum structure: folder, file, and header
A plugin can be a single PHP file inside wp-content/plugins, but the recommended practice is to create its own folder from the beginning. For our example, we will build a real and useful plugin: a customizable notice at the top of the website, with an included settings page. The structure will be this:
wp-content/plugins/bw-aviso-superior/
├── bw-aviso-superior.php (archivo principal)
├── includes/
│ └── class-bw-aviso.php (lógica del plugin)
└── assets/
└── css/
└── aviso.css (estilos del aviso)
The only essential thing for WordPress to recognize the plugin is the plugin header: a comment block at the beginning of the main file. Create bw-aviso-superior.php with this content:
<?php
/**
* Plugin Name: BW Aviso Superior
* Plugin URI: https://biblioweb.es/
* Description: Muestra una barra de aviso personalizable en la parte superior de la web.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: BiblioWeb
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: bw-aviso-superior
*/
// Seguridad: impedir el acceso directo al archivo.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'BW_AVISO_VERSION', '1.0.0' );
define( 'BW_AVISO_PATH', plugin_dir_path( __FILE__ ) );
define( 'BW_AVISO_URL', plugin_dir_url( __FILE__ ) );
require_once BW_AVISO_PATH . 'includes/class-bw-aviso.php';
// Arrancar el plugin cuando WordPress haya cargado los plugins.
add_action( 'plugins_loaded', array( 'BW_Aviso', 'init' ) );
With just this file (and the class we'll see now), the plugin will already appear in the list of Plugins of the dashboard, ready to be activated. Note the check for ABSPATH: prevents someone from directly executing the file by typing its URL, a basic security measure that should open all your PHP files.
Hooks: the heart of any plugin
WordPress is built on a system of hooks (hooks) that allows your code to attach to specific moments of execution. Without hooks, there is no plugin: they are the official mechanism to intervene without modifying the core. There are two types:
Actions: do something at a given moment
A action executes your function when an event occurs: WordPress finishes loading, a post is published, the footer is rendered. They are used with add_action():
add_action( 'wp_footer', 'bw_mensaje_en_footer' );
function bw_mensaje_en_footer() {
echo '<!-- Generado por BW Aviso Superior -->';
}
Filters: modify data before it is used
A filter receives a value, transforms it, and returns it. WordPress uses it for everything: a post's title, content, excerpt length. They are used with add_filter():
add_filter( 'excerpt_length', 'bw_extracto_corto' );
function bw_extracto_corto( $length ) {
return 25; // palabras del extracto
}
The key difference: an action does things and returns nothing; a filter always must return the value (modified or not). Forgetting the return in a filter is one of the most common mistakes when starting and can leave empty texts all over the website.
The complete plugin: main class with settings and output
Now let's move on to the central piece. Create includes/class-bw-aviso.php with the class that registers the settings, renders the options page, and displays the notice on the public side:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class BW_Aviso {
const OPTION = 'bw_aviso_opciones';
public static function init() {
$instancia = new self();
// Parte pública.
add_action( 'wp_body_open', array( $instancia, 'mostrar_aviso' ) );
add_action( 'wp_enqueue_scripts', array( $instancia, 'cargar_estilos' ) );
// Administración.
add_action( 'admin_menu', array( $instancia, 'registrar_pagina_ajustes' ) );
add_action( 'admin_init', array( $instancia, 'registrar_ajustes' ) );
}
public function mostrar_aviso() {
$opciones = get_option( self::OPTION );
if ( empty( $opciones['activo'] ) || empty( $opciones['texto'] ) ) {
return;
}
printf(
'<div class="bw-aviso-superior">%s</div>',
esc_html( $opciones['texto'] )
);
}
public function cargar_estilos() {
$opciones = get_option( self::OPTION );
if ( empty( $opciones['activo'] ) ) {
return;
}
wp_enqueue_style(
'bw-aviso-superior',
BW_AVISO_URL . 'assets/css/aviso.css',
array(),
BW_AVISO_VERSION
);
}
public function registrar_pagina_ajustes() {
add_options_page(
'Aviso Superior',
'Aviso Superior',
'manage_options',
'bw-aviso-superior',
array( $this, 'render_pagina_ajustes' )
);
}
public function registrar_ajustes() {
register_setting(
'bw_aviso_grupo',
self::OPTION,
array( 'sanitize_callback' => array( $this, 'sanear_opciones' ) )
);
}
public function sanear_opciones( $entrada ) {
return array(
'activo' => ! empty( $entrada['activo'] ) ? 1 : 0,
'texto' => isset( $entrada['texto'] )
? sanitize_text_field( $entrada['texto'] )
: '',
);
}
public function render_pagina_ajustes() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$opciones = get_option( self::OPTION, array( 'activo' => 0, 'texto' => '' ) );
?>
<div class="wrap">
<h1>Aviso Superior</h1>
<form method="post" action="options.php">
<?php settings_fields( 'bw_aviso_grupo' ); ?>
<table class="form-table">
<tr>
<th scope="row">Mostrar aviso</th>
<td>
<label>
<input type="checkbox"
name="<?php echo esc_attr( self::OPTION ); ?>[activo]"
value="1" <?php checked( 1, $opciones['activo'] ); ?> />
Activar la barra de aviso
</label>
</td>
</tr>
<tr>
<th scope="row">Texto del aviso</th>
<td>
<input type="text" class="regular-text"
name="<?php echo esc_attr( self::OPTION ); ?>[texto]"
value="<?php echo esc_attr( $opciones['texto'] ); ?>" />
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
}
And finally the styles, in assets/css/aviso.css:
.bw-aviso-superior {
background: #1d2327;
color: #ffffff;
text-align: center;
padding: 10px 16px;
font-size: 15px;
}
Activate the plugin, go to Settings → Top Notice, check the box, type some text, and save. The bar will appear at the top of your website. You have now achieved create a WordPress plugin complete: with a settings page, options saved in the database, custom styles, and output on the public side.
Best practices that make a difference
The previous example already applies several rules that you should internalize. Let's review them along with others equally important:
Sanitize input and escape output
All incoming data (forms, URL, APIs) is sanitized with functions like sanitize_text_field(), and all data displayed on screen is escaped with esc_html(), esc_attr() o esc_url(). This pair of habits prevents the vast majority of XSS vulnerabilities. If you are interested in the defensive approach, we have a guide with 7 tricks to strengthen your WordPress security which complements very well what you do at the code level.
Unique prefixes everywhere
Functions, classes, options, and style handles must have their own prefix (in our case bw_ y BW_). PHP does not allow two functions with the same name: if your plugin declares enviar_email() and another plugin also, the website will crash with a fatal error.
Check capabilities and use nonces
Before displaying or operating on administration pages, verify permissions with current_user_can(). In custom forms, add nonces with wp_nonce_field() and verify them when processing. The settings API we use (settings_fields()) already manages the nonce for you, another reason to prefer it over processing forms manually.
Load resources only when needed
Our CSS is only enqueued if the notice is active. Apply the same criterion always: a plugin that loads scripts on all pages unnecessarily penalizes the performance of the entire website. In fact, many of the speed problems attributed to WordPress are actually poorly written plugins; in the guide on how to speed up WordPress with PHP settings you can see the real impact of these decisions.
How to test and debug your plugin
With WP_DEBUG activated, your workflow will be: save the file, reload the page, and check wp-content/debug.log if something doesn't work. Some additional tips:
- Test activation and deactivation. Activate and deactivate the plugin several times, checking that it doesn't throw warnings.
- Test with other active plugins. Conflicts between plugins are the main source of real incidents.
- Test with different users. Log in with an editor or subscriber and confirm that they don't see the settings page.
- Use
error_log()as a cheat sheet. Writingerror_log( print_r( $variable, true ) );at a point in the code shows you the content of any variable in the log.
If your plugin is going to handle structured data (for example, API responses in JSON), a tool like the BiblioWeb JSON formatter and validator will save you time when inspecting and validating those responses during development.
Next steps: grow with order
From this base, you can extend the plugin in many directions: add a color picker for the bar, schedule start and end dates for the notice, create a shortcode, or expose options in the REST API. When the project grows, maintain structural discipline: the logic in includes/, the resources in assets/, and the main file only as an entry point.
The essential reference for in-depth study is the Official WordPress Plugin Handbook, which documents everything from available hooks to the directory publication process. And if you want to see how the big players solve things, nothing beats reading the code of established plugins: the ones we review in the 5 essential WordPress plugins for beginners are a good starting point for studying structure and style.
Frequently Asked Questions
Do I need to know a lot of PHP to create a WordPress plugin?
Not to start. With variables, functions, arrays, and conditionals, you can build useful plugins like the one in this guide. The development itself will naturally and progressively lead you to more advanced concepts (classes, namespaces, APIs).
Where are plugins stored in WordPress?
In the folder wp-content/plugins of your installation. Each plugin occupies its own subfolder (or a single PHP file in very simple cases). WordPress automatically detects any file with a valid plugin header within that path.
Can I break my website when developing a plugin?
A PHP syntax error in an active plugin can bring down the website, which is why development is always done in a testing environment. If it happens in production, simply renaming the plugin folder via SFTP is enough to deactivate it and instantly restore the site.
How do I publish my plugin in the official directory?
You must comply with the directory guidelines (GPL license, secure code, no obfuscation), prepare a file readme.txt and submit it for review from wordpress.org. After approval, you receive access to an SVN repository from which versions are distributed.
Conclusion
Creating a WordPress plugin is not exclusive territory for veteran programmers: it's a folder, a header, and a handful of well-chosen hooks. In this guide, you've built a complete one, with a settings page, data sanitization, conditional styles, and the security best practices used by professional plugins. The leap in quality compared to pasting code into the theme is enormous: your functionality is now portable, updatable, and isolated from the design. The best final advice is simple: choose a small, real problem on your own website and solve it with a plugin. There's no better school than maintaining your own code running on a real site.