Yes, WordPress plugin development starts with a single PHP file, a header comment, and hooks that connect your code to WordPress.
Building a small add-on for WordPress is less scary than it looks. You’ll create a folder, add a main PHP file with a header, wire in hooks, and ship features in tiny steps. This guide walks through the process from zero to a working add-on, with clean structure, safety tips, and release prep.
Developing A WordPress Plugin Step By Step
Start with a project name that reads well as a folder and a PHP filename. Avoid spaces. Use dashes or underscores. Keep everything under wp-content/plugins/. The folder name usually matches the main file name.
Create a minimal main file that registers your add-on with a standard header. WordPress scans headers to list your tool on the Plugins screen. Then add a simple hook so you can see output on the page.
<?php
/**
* Plugin Name: Sample Notes
* Description: Adds a [sample_note] shortcode that prints a note.
* Version: 0.1.0
* Author: Your Name
* License: GPL-2.0-or-later
* Text Domain: sample-notes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function sn_register_shortcode() {
add_shortcode( 'sample_note', function( $atts, $content = '' ) {
$text = $content ? $content : 'Hello from Sample Notes';
return '<div class="sample-note">' . esc_html( $text ) . '</div>';
} );
}
add_action( 'init', 'sn_register_shortcode' );
Drop that file in your plugin folder and flip the switch on the Plugins screen. Add [sample_note] in a post to verify everything loads. Seeing that box appear proves your header, action, and shortcode are wired correctly.
Starter Plugin Folder Map
| Path | Role | Notes |
|---|---|---|
| /wp-content/plugins/sample-notes/ | Plugin Root | Folder name matches main file. |
| sample-notes.php | Main File | Header, defines constants, loads rest. |
| includes/ | Core Code | Functions, classes, hooks. |
| assets/css/ | Styles | Only enqueue on pages that need it. |
| assets/js/ | Scripts | Register and enqueue with versions. |
| languages/ | Translations | .pot, .mo, .po files. |
| uninstall.php | Cleanup | Delete options on removal. |
| readme.txt | Directory Info | Changelog, tested up to, tags. |
Setting Up A Reliable Dev Environment
Spin up a local site with a stack you like: Docker, Local, or a simple PHP/MySQL server. Match the PHP version you plan to support. Turn on WP_DEBUG and WP_DEBUG_LOG. Keep a fresh database snapshot so you can roll back fast. Add WP-CLI for quick commands and database imports. If you lean on build steps, keep Node and Composer pinned to versions and commit lock files so team members get the same output.
Create a test site with a default theme, plus a second site with a popular theme. This surfaces CSS collisions and editor quirks early. Keep screenshots or short notes on anything odd you see so you can retest before release.
Name, Constants, And Autoloading
Pick a short prefix to avoid function collisions. A constant for the version and the base path helps with enqueues and cache busting. If you prefer classes, use a simple autoloader or Composer.
<?php
define( 'SN_VERSION', '0.1.0' );
define( 'SN_FILE', __FILE__ );
define( 'SN_PATH', plugin_dir_path( SN_FILE ) );
define( 'SN_URL', plugin_dir_url( SN_FILE ) );
function sn_enqueue_assets() {
wp_enqueue_style( 'sn-style', SN_URL . 'assets/css/style.css', [], SN_VERSION );
wp_enqueue_script( 'sn-app', SN_URL . 'assets/js/app.js', [ 'wp-i18n' ], SN_VERSION, true );
}
add_action( 'wp_enqueue_scripts', 'sn_enqueue_assets' );
Actions, Filters, And Data Flow
Hooks let your code run at the right moment. Actions fire events. Filters modify values. Most features are a mix of both. Keep hook callbacks small; call into separate functions for clarity.
<?php
add_action( 'admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>Sample Notes loaded.</p></div>';
} );
add_filter( 'the_content', function( $content ) {
if ( is_singular() ) {
$badge = '<p class="sn-badge">Powered by Sample Notes</p>';
return $content + $badge;
}
return $content;
} );
Settings Page Without Bloat
Store options with the Settings API. Create a single menu entry, register one option array, and output fields. Sanitize on save. Keep defaults in code so a fresh install behaves predictably.
<?php
function sn_register_settings() {
register_setting( 'sn_settings', 'sn_options', 'sn_sanitize_options' );
add_settings_section( 'sn_main', 'Sample Notes', '__return_false', 'sn_settings' );
add_settings_field(
'sn_default_text',
'Default Text',
'sn_default_text_field',
'sn_settings',
'sn_main'
);
}
add_action( 'admin_init', 'sn_register_settings' );
function sn_default_text_field() {
$opts = get_option( 'sn_options', [ 'default_text' => 'Hello' ] );
echo '<input type="text" name="sn_options[default_text]" value="' . esc_attr( $opts['default_text'] ) . '" />';
}
function sn_sanitize_options( $opts ) {
$opts['default_text'] = sanitize_text_field( $opts['default_text'] ?? '' );
return $opts;
}
function sn_add_menu() {
add_options_page( 'Sample Notes', 'Sample Notes', 'manage_options', 'sn-settings', 'sn_render_settings' );
}
add_action( 'admin_menu', 'sn_add_menu' );
function sn_render_settings() {
echo '<div class="wrap"><h1>Sample Notes</h1>';
echo '<form method="post" action="options.php">';
settings_fields( 'sn_settings' );
do_settings_sections( 'sn_settings' );
submit_button();
echo '</form></div>';
}
Code Quality: Linters And Standards
Add a PHP_CodeSniffer ruleset for WordPress. Run it in CI and locally. Keep a phpcs.xml at the root so the team shares the same rules. Use a formatter in your editor to keep spacing and naming consistent. This saves time in code review and prevents churn in diffs.
Adopt a small checklist: no direct access to superglobals without validation, escape on output, nonces on every write action, and no calls that fetch remote data during page render. Keep a quick README for contributors with commands for setup and testing.
Internationalization And Accessibility
Load a text domain so strings can be translated. Wrap UI text with i18n helpers. For markup, add labels, alt text, and focus styles. Keep color contrast readable.
<?php
function sn_load_textdomain() {
load_plugin_textdomain( 'sample-notes', false, dirname( plugin_basename( SN_FILE ) ) . '/languages' );
}
add_action( 'plugins_loaded', 'sn_load_textdomain' );
Database Writes The Safe Way
Use built-in APIs for storage. For options, use get_option and update_option. For custom tables, use $wpdb with prepared queries. Escape on output. Validate before save. Never trust raw input.
Security Musts You Should Bake In
Check capabilities on admin screens. Use nonces on every POST. Sanitize every field. Escape on output. Avoid direct file access by exiting when ABSPATH is not defined. Keep dependencies pinned and review changelogs before bumping versions.
Versioning, Changelog, And Readme
Pick a simple version scheme like 0.1.0, 0.2.0, 1.0.0. Update the changelog with plain language notes on each release. Your readme should include “Tested up to,” a short description, and a basic usage section with a shortcode or block name.
Common Hooks Cheat Sheet
| Hook | When It Fires | Typical Use |
|---|---|---|
| init | After WordPress loads | Register post types, shortcodes. |
| admin_menu | Admin menu build | Add settings pages. |
| wp_enqueue_scripts | Front-end assets | Load CSS and JS. |
| enqueue_block_editor_assets | Block editor | Editor-only scripts. |
| the_content | Before display | Filter article HTML. |
| uninstall.php | On removal | Delete plugin data. |
Testing On A Local Copy
Work on a local site so you can break things safely. Keep WP_DEBUG enabled during development. Add basic PHP unit tests if your plugin has logic that benefits from repeatable checks. Exercise all settings pages and uninstallation paths.
Try updates from older versions on a throwaway site. Add upgrade routines behind version checks so stored data migrates. Keep backups of sample data to replay real flows during tests.
Performance Tips That Keep Sites Snappy
Load assets only where needed. Defer heavy scripts. Cache results for remote calls with transients. Avoid blocking calls on page render. Keep database queries indexed and lean. Profile with Query Monitor when pages feel slow.
When adding an admin screen, lazy-load large tables. Paginate results and avoid SELECT *. Scope styles so you don’t override theme CSS on public pages.
Release Prep And Distribution
Before shipping, bump the version in both the main file and the readme. Tag the release in your VCS. Test on a fresh site with only a default theme and your plugin. If you plan to publish in the directory, follow naming, branding, and code standards. Keep your banner images small and crisp.
Ship a minimal MVP first. Keep the settings simple. Gather feedback. Small, steady updates are easier to review and safer to roll back if a bug slips in.
Live Updates And Backward Compatibility
Guard against breaking changes. When removing a feature, provide a deprecation path and log admin notices with clear guidance. Use version checks in upgrade routines so old installs can migrate cleanly.
When changing a filter or action signature, keep the old one in place for a few releases with a wrapper. Add a small note in the changelog so site owners know what changed.
Support-Ready Error Handling
Return helpful messages when something fails. When catching WP_Error, show a short line to admins and log the full code. Place logs behind capability checks. Never echo raw stack traces on public pages.
Wrap remote requests with timeouts. Add retries only when it helps users. For noisy failures, add a dismissible notice with a link to settings where a fix lives.
Clean Uninstall
When a user deletes your plugin from the Plugins screen, remove stored data if that’s the promise. A dedicated uninstall.php file is the simplest route. Avoid leaving orphaned options and transients.
<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'sn_options' );
Safe Patterns For UI And Blocks
For block-based features, register blocks with metadata files and enqueue only editor assets in the editor. For classic screens, stick to core UI patterns so users feel at home. Avoid custom admin frameworks unless they solve a clear need.
Keep labels short. Add help text under fields when a choice is not obvious. Use color only as a hint; pair it with text so messages remain clear to all users.
Two Handy Building Blocks
Here are two small snippets you’ll reuse: a nonce check for forms and a helper to add an admin notice.
<?php
// Nonce check
if ( isset( $_POST['sn_nonce'] ) && wp_verify_nonce( $_POST['sn_nonce'], 'sn_action' ) ) {
// Safe to process
}
// Admin notice helper
function sn_notice( $msg, $type = 'success' ) {
$type = $type === 'error' ? 'error' : 'success';
printf( '<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>', $type, esc_html( $msg ) );
}
Privacy, Telemetry, And Consent
Skip tracking by default. If you add usage data, make it opt-in with a clear toggle and a short sentence on what is collected. Respect site privacy settings. Never send personal data to third-party services without consent.
What To Read Next
For a detailed guide on structure and headers, see the Plugin Basics page. For safe coding patterns and review steps, the security guide from the same handbook is a solid companion.
Ship the smallest useful feature, learn from real sites, and refine. Keep code tidy, keep hooks small, and your add-on will feel native to WordPress.