Most WordPress developers start writing plugins the same way I did: by pasting snippets into a theme’s functions.php until it becomes an unmanageable mess. Eventually you realise that code adding features to a site doesn’t belong in the theme, and you move it into a plugin.
Writing a plugin is less intimidating than it sounds. A plugin can be a single file with a few lines of code. But a little structure and a few good habits from the start make the difference between a plugin you’re proud of and one that breaks sites, conflicts with others or opens security holes.
Here’s how to write your first plugin properly.
What a plugin is
A plugin is PHP code in its own folder inside wp-content/plugins, with a special comment header that tells WordPress what it is. WordPress loads active plugins early on every request, before the theme, and plugins add or change functionality through hooks.
Good reasons to write a plugin include:
- Functionality that should survive theme changes, like custom post types, shortcodes, integrations and business logic.
- Reusable features you want to use on several sites.
- Customisations you want under version control, separate from the theme.
The plugin header
Create a folder, such as wp-content/plugins/mysite-tools, and a main file inside it with the same name, mysite-tools.php. The header comment is all WordPress needs to recognise it:
The ABSPATH check stops the file from running if someone requests it directly in a browser. Include it at the top of every PHP file in your plugin.
The plugin now appears under Plugins in the dashboard, ready to activate.
Structure for growth
For a small plugin, one file is fine. As it grows, a simple structure keeps things organised:
Define a few constants in the main file for paths and the version:
Prefix everything
WordPress loads all plugins into the same PHP environment. If two plugins define a function called add_custom_styles, PHP throws a fatal error and the site breaks.
Avoid collisions by prefixing everything that’s global: functions, classes, constants, option names, hook names, post types, transients and database tables. Use a short, unique prefix, like mysite_ or mst_. Alternatively, use PHP namespaces for functions and classes, but remember options, hooks and database names still need prefixes.
A simple working example
Let’s build something practical: a plugin that adds a “Reading time” estimate to blog posts, with a setting for words per minute.
| if ( ! is_singular( 'post' ) | ! in_the_loop() | ! is_main_query() ) { |
|---|
Notice the checks at the start. Without them, the reading time would appear in excerpts, widgets and anywhere else the_content filter runs. Filtering content precisely is a common area for bugs.
Adding a settings page
For a simple option, the Settings API registers settings and fields in a way that handles saving and security for you:
This adds a field to the existing Settings, then Reading screen. For plugins with several settings, you’d create a dedicated settings page with add_options_page, but the same principles apply: register settings with a sanitize callback and let WordPress handle the form processing and nonces.
Activation, deactivation and uninstall
Plugins often need to set things up when activated and clean up when removed.
Deactivation should stop things running, such as scheduled events, but shouldn’t delete data. Users often deactivate temporarily while troubleshooting.
Deletion is different. Use an uninstall.php file to remove options, custom tables and other data when the plugin is deleted:
Leaving data behind after uninstall is one of the main causes of database clutter on older sites. Be a good citizen and clean up.
Security fundamentals
Every plugin must handle data safely. Four principles cover most of it.
Sanitize input
Clean any data coming in, from forms, URLs or APIs, before using or saving it. Use functions like sanitize_text_field, sanitize_email, absint and esc_url_raw.
Escape output
Escape data when you output it, using esc_html, esc_attr, esc_url or wp_kses_post depending on context. Escape late, right at the point of output.
Check permissions
Before performing any action, confirm the user is allowed to do it with current_user_can.
Verify intent with nonces
For forms and actions that change data, use nonces to confirm the request came from your site and was intended by the user. The Settings API handles this automatically. For custom forms and AJAX, use wp_nonce_field and check_admin_referer, or wp_verify_nonce.
Use prepared statements
When querying the database directly, always use $wpdb->prepare for any variable data.
These practices prevent the vast majority of plugin vulnerabilities. My separate guide on WordPress coding standards and security functions covers them in more depth.
Loading assets properly
Never hard-code script or style tags. Enqueue them, and only where needed:
Using the plugin version in the version argument means browsers fetch the new file when you update the plugin.
Make it translatable
Wrap user-facing strings in translation functions, like __(), _e(), esc_html__() and _n(), using your text domain. Since WordPress 4.6, plugins hosted on WordPress.org have translations loaded automatically; for others, load the text domain yourself. Even if you only need English today, translation-ready code costs nothing and makes your plugin usable more widely.
Follow the coding standards
WordPress has official coding standards for PHP, JavaScript, CSS and HTML. Following them makes your code easier for others to read and catches common problems. PHP_CodeSniffer with the WordPress Coding Standards ruleset checks your code automatically, and most code editors can run it as you type.
Test before you ship
- Test with WP_DEBUG enabled and fix every notice and warning.
- Test with a default theme to make sure you don’t rely on theme-specific code.
- Test with other popular plugins active.
- Test activation, deactivation and deletion.
- Test with the minimum WordPress and PHP versions you declare.
For larger plugins, automated tests with PHPUnit and WordPress’s test framework catch regressions as the plugin grows.
Version control from day one
Put your plugin in Git before you write the second line of code. Commit small, focused changes with clear messages. When something breaks, you can see exactly what changed and when, and you can roll back in seconds. Git also makes it easy to share your plugin with collaborators, deploy it to sites and keep a record of why decisions were made.
A simple .gitignore that excludes build artefacts, dependency folders like node_modules and vendor if you build them during deployment, and any local configuration files keeps the repository clean.
Versioning and changelogs
Use semantic versioning: increase the patch number for bug fixes, the minor number for new features that don’t break anything and the major number for changes that might break existing setups. Update the Version line in the plugin header and your version constant together, so asset cache busting works correctly.
Keep a changelog, even for private plugins. A short list of what changed in each version helps you and anyone else understand the plugin’s history, and it makes troubleshooting much easier when a site reports a problem after an update.
Think about performance early
Plugins run on every request, so small inefficiencies add up. A few habits keep your plugin light:
- Only load admin code in the admin, using is_admin checks or admin-specific hooks.
- Only enqueue assets on pages that need them.
- Avoid running database queries on every page load when the result rarely changes; cache it with transients or the object cache.
- Store settings with autoload turned off if they’re only needed occasionally, so they don’t add to every request.
- Never call external APIs synchronously on front-end page loads. Fetch data in the background with scheduled tasks and cache it.
Test your plugin with Query Monitor active and check how many queries and how much time it adds to a typical page. If a simple plugin adds dozens of queries, something can be improved.
Frequently asked questions
Should custom functionality go in a plugin or functions.php?
Functionality that should survive a theme change belongs in a plugin. Code that’s purely about the theme’s presentation can stay in functions.php.
Do I need to use object-oriented PHP?
No. Simple plugins work well with prefixed functions. As plugins grow, classes and namespaces help organise code, but they’re not required.
How do I share my plugin on WordPress.org?
Follow the plugin guidelines, prepare a readme.txt in the required format, submit it for review and, once approved, publish through the Subversion repository provided. Reviewers check security and guideline compliance.
What’s a must-use plugin?
A plugin placed in wp-content/mu-plugins that loads automatically and can’t be deactivated from the dashboard. It’s useful for essential site functionality that should never be switched off accidentally.
Final thoughts
Writing a good plugin is mostly about habits: a clear header, a sensible structure, prefixes on everything, hooks used precisely, careful activation and uninstall routines, and security handled at every step. Start small, follow the coding standards and test properly. Your first plugin might be a reading time estimate, but the same foundations will carry you to much bigger projects.




