How to Build a WordPress Plugin & Publish to WordPress: Why I Created this Guide
- 1 How to Build a WordPress Plugin & Publish to WordPress: Why I Created this Guide
- 2 The WordPress Plugin Development Lifecycle
-
3
WordPress Plugin Coding Standards & Best Practices
- 3.1 File Structure and Initial Setup
- 3.2 Data Sanitization and Validation (Input Handling)
- 3.3 Output Escaping (Displaying Data Safely)
- 3.4 Database Queries and $wpdb (Preventing SQL Injection)
- 3.5 Nonces and User Capabilities (Security Checks for Actions)
- 3.6 Using WordPress APIs and Libraries (Don’t Reinvent the Wheel)
- 3.7 Enqueuing Scripts and Styles Properly
- 3.8 Internationalization (Making Plugin Translatable)
- 3.9 Other WordPress Plugin Security & Quality Considerations
- 4 Common Plugin Rejection Reasons & How to Avoid Them
- 5 Submission & Release (SVN) Steps
- 6 How to Publish a Plugin to WordPress Using AI: A Developer Markdown Checklist

How to Build & Publish a WordPress Plugin
When we talk about how to build a WordPress plugin, it typically falls into one of two categories:
- Personal requirements and needs – We may want some unique, custom functionality for our own website that isn’t readily available in existing plugins. This scenario is the most common, as many website owners routinely seek to enhance or tailor their WordPress sites by adding new, innovative features.
- Sharing, community and monetisation – The second scenario involves developing a plugin intended for public release on the official WordPress.org plugin directory. This approach, although less common, is often pursued by developers and businesses with an open-source ethos who aim to contribute back to the vibrant WordPress community, or those seeking an additional revenue stream through premium features or subscriptions attached to their plugins.
Developing a Plugin for Personal Use
The reality is, anyone equipped with basic PHP coding skills, or even aided by modern AI coding assistants like Roo Code, Augment, or Claude Code, can readily develop a straightforward WordPress plugin for personal use.
A quick online search will provide numerous tutorials and resources to help individuals understand how to develop their first WordPress plugin. For more intricate or highly custom plugin developments however, the expertise of a professional WordPress developer will often become necessary as there is only so far you’ll be able to get using tutorials alone.
Developing a Plugin for the WordPress Directory
The process of publishing a plugin to the WordPress.org directory is significantly more complex.
Anyone who has attempted this will know firsthand the challenges involved. WordPress.org maintains rigorous WordPress plugin coding standards and compliance requirements. Even well-written plugins typically undergo multiple rounds of meticulous moderation, often with differing or even conflicting feedback from various moderators.

Facing WordPress plugin rejections is hard on the best of us
This experience can be daunting even for seasoned PHP developers who may not be familiar with the specific WordPress development standards required. Unfortunately, many developers begin the process of publishing their plugin on WordPress.org but ultimately abandon their efforts due to frustration and complexity.
My Attempt to Publish a Plugin to WordPress
This scenario was exactly what I experienced a few years ago.
At the time, I created what I considered an innovative, highly functional, and genuinely useful plugin, fully intending to share it with the WordPress community for free. When the WordPress moderation team initially rejected my plugin, I felt shocked and honestly a bit offended. Undeterred, I underwent multiple rounds of refactoring to comply with the WordPress plugin standards. Despite my efforts, approval continued to elude and frustrate me.
Ultimately, I sought professional assistance from a reputable industry partner known for successfully developing and publishing paid WordPress plugins. With their expertise, my plugin eventually met the stringent coding standards expected of the WordPress.org moderation team and secured approval.
The journey was painful, frustrating, and expensive. One of the major hurdles I faced was the lack of comprehensive resources online. While countless tutorials cover how to build a WordPress plugin, I found few detailed, practical guides on precisely how to get a plugin officially published and accepted into the WordPress.org directory. Apart from fragmented guides and official documentation, I had to piece together disparate advice, ultimately falling short on my own.
Why I Have Created this Guide
To spare others the headaches and setbacks I encountered, I have put together this guide to document absolutely everything you needed to know to publish a plugin to WordPress.
You’ll find a complete and comprehensive resource that covers everything you need to know on how to build and publish a WordPress plugin. This guide meticulously outlines a WordPress compliant plugin development process, ensuring your plugin adheres precisely to WordPress coding standards to enhance your chances of first-time acceptance on the WordPress.org plugin directory.
Developing a WordPress Plugin using AI?
For those using AI-assisted development tools like the ones mentioned above, I’ve attached a practical markdown document that serves as a clear, concise checklist of WordPress plugin coding standards to facilitate coding, debugging, and thorough testing.
Whether you’re a developer just starting out with creating your first plugin for the WordPress directory or using AI coding tools to streamline or enhance your development process, this guide aims to be invaluable, streamlining your path to publishing success.
The WordPress Plugin Development Lifecycle
Developing a WordPress plugin for the official WordPress.org Plugin Directory involves several stages, from initial setup and coding to final SVN release and maintenance. At each stage, there are important standards and best practices to follow to ensure your plugin meets WordPress coding standards and repository guidelines. Below is an overview of the lifecycle:
- Planning & Setup: Determine your plugin’s purpose and ensure it doesn’t violate any repository rules (e.g. no trademark issues in the name, GPL-compatible license). Set up a development environment with WordPress and enable debugging. Create a new plugin folder and main PHP file (with a proper header), using a unique plugin slug.
- Development & Coding: Write the plugin’s code following WordPress coding standards and security best practices. Use WordPress APIs and hook your functionality into WordPress actions/filters, rather than executing code directly. Implement features gradually and test thoroughly.
- Hardening & Testing: Audit your code for security (sanitize inputs, escape outputs, use nonces, etc.) and performance issues. Test the plugin in different scenarios and WordPress versions. Ensure all repository compliance requirements are met (licensing, no phoning-home without consent, etc.).
- Preparation for Release: Update the plugin’s metadata (version, author, readme.txt) and documentation. Generate a readme.txt file following the WordPress plugin readme standard (including “Requires at least”, “Tested up to”, “Stable tag”, etc.). Optionally, run the official Plugin Check tool to catch common issues before submission.
- Submission to Repository: Submit your plugin via WordPress.org (you’ll upload a ZIP of your plugin). As of late 2024, you must have Two-Factor Authentication (2FA) enabled on your WordPress.org account to submit a plugin. Your submission will be auto-scanned by the Plugin Check tool for basic issues (like version mismatches or invalid tags). If errors are found, you’ll need to fix them before it can be reviewed.
- Review Process: Once submitted, the plugin goes into a review queue. A human reviewer will check your code for compliance with all guidelines. This may take days or even weeks (there can be a backlog). The reviewer might email you with required changes if any issues are found (security flaws, guideline violations, etc.). Commonly, authors are asked to self-correct a few basic errors that account for 95% of rejections.
- Approval & SVN Access: If your plugin is approved, you’ll receive an email with SVN repository access details for your plugin (a unique SVN URL like
https://plugins.svn.wordpress.org/your-plugin-slug
). You will use Subversion to manage releases (trunk, branches, tags) on the WordPress repository. - Final Release via SVN: Using SVN, you’ll upload your plugin code to the
trunk
directory and create a tag for the initial release (usually/tags/1.0.0/
for version 1.0.0, for example). You must set the “Stable Tag” in readme.txt to point to that tagged version. After committing, the plugin will go live in the directory (the plugin page pulls information from the tagged release and your readme). - Maintenance: Post-release, update your plugin as needed. Follow the versioning and tagging procedure for each update (bump version, commit to trunk, tag new version, update Stable Tag). Keep the “Tested up to” value current to reassure users. Respond to support and prepare for possible future reviews if you make major changes. Any security issues should be fixed promptly, as the plugin team may close plugins with unresolved vulnerabilities.
Each phase requires attention to specific requirements, which we’ll detail below. By following this guide and checklist, you can ensure your plugin fully complies with WordPress.org standards from the first line of code to the moment users install it.
WordPress Plugin Coding Standards & Best Practices
Building a WordPress plugin for the official repository means adhering to strict coding standards and security practices. The plugin review team will scrutinise your code for any security issues or deviations from recommended WordPress APIs. Below are detailed best practices you must follow in your code to meet these requirements.

Always begin with understanding WordPress plugin coding standards
File Structure and Initial Setup
- Main Plugin File & Header: Your plugin should have a single main PHP file (often named after your plugin slug) located in its own folder. The main file must contain a standard plugin header comment with at least the plugin name, and typically version, author, license, etc. For example:
Ensure the Plugin Name is unique and does not infringe trademarks (don’t start it with “WordPress” or a known brand unless you are authorised). The Text Domain should exactly match your plugin’s slug (this is crucial for translations to work and is checked during review). Include a valid license (GPLv2 or later is strongly recommended) and make sure all included code (yours or third-party) is GPL-compatible.
- Unique Namespace or Prefix: All your plugin’s functions, classes, variables, and constants should be uniquely named to avoid collisions with WordPress or other plugins. A common practice is to prefix everything with a short identifier (often derived from your plugin name). For example, if your plugin is “Easy Custom Posts”, use
ecp_
orECP
as a prefix (ecp_activate()
, classECP_Admin
, etc.). Do not use generic names or overly short prefixes likewp_
or_
that are reserved for WordPress core. Ensuring unique prefixes prevents fatal errors or unexpected behavior if two plugins use the same function names. Avoid wrapping functions inif (function_exists())
as a primary solution – if a name collision occurs, it means your function wouldn’t load at all, breaking your plugin. It’s better to use truly unique names from the start. - File Organization: Organise your plugin files logically. Apart from the main file, consider sub-folders for includes, assets (CSS/JS), templates, etc. When including PHP files, never hard-code paths to the
wp-content
directory; use WordPress functions to determine paths dynamically. For example, useplugin_dir_path(__FILE__)
to get the file path of your plugin andplugin_dir_url(__FILE__)
orplugins_url()
to get URL for assets. This ensures your plugin works even if WordPress’ content directory is relocated by the user (which is allowed). Always reference files via these functions (or constants like__DIR__
) instead of assumptions about directory structure. - Guarding Direct Access: Prevent direct access to PHP files in your plugin. It’s common to add a line at the top of each executable PHP file (especially the main file and any includes) to exit if the file is accessed outside of WordPress. For example:
This check (
defined('ABSPATH')
) ensures the file runs only in the context of a loaded WordPress environment. Directly accessing plugin files via URL can lead to security issues, unpredictable behavior, or revealing sensitive info. The plugin team expects this protection; failing to include it is a common issuethat can cause rejection. - No Calling WordPress Core Files Directly: Do not include or require core WordPress files like
wp-load.php
orwp-config.php
. Your plugin code should be executed through WordPress hooks, not by manually bootstrapping WordPress. Including core files is prone to failure (as not all installs have the same structure) and is explicitly disallowed unless absolutely necessary. Instead, if you need to trigger functionality (e.g., process a form or an AJAX request), hook into appropriate actions (such asinit
,admin_init
, or AJAX hooks). For example, if you were tempted to load WordPress to handle an AJAX call, use WordPress’ AJAX API (wp-admin/admin-ajax.php
withadd_action('wp_ajax_your_action', 'function')
) or the REST API, rather than calling core files. Tying your logic to WordPress’s event hooks ensures WordPress is loaded properly and avoids breakage. - Plugin Activation/Deactivation Hooks: If your plugin needs to perform setup or cleanup tasks (like creating custom database tables, flushing rewrite rules, etc.), use the provided activation/deactivation hooks. For example,
register_activation_hook(__FILE__, 'my_plugin_activate')
andregister_deactivation_hook(__FILE__, 'my_plugin_deactivate')
. Within these callbacks, keep operations minimal and always check capabilities if creating content or options. If you add database tables or options, also implement an uninstall routine (viaregister_uninstall_hook
or anuninstall.php
file) to clean up if the user deletes the plugin. While not strictly required for approval, providing an uninstall cleanup is a best practice that demonstrates quality.
Data Sanitization and Validation (Input Handling)
“All input data must be sanitized, validated, and escaped appropriately.” This is a fundamental rule for WordPress development. The goal is to prevent malicious or malformed data from causing security issues (like XSS or SQL injection) or breaking the plugin. Here’s how to handle input safely:
- Sanitize Early: As soon as you receive data (from
$_POST
,$_GET
,$_REQUEST
, cookies, etc.), sanitize it. Sanitization means cleaning or stripping out unwanted characters and dangerous content. WordPress provides many helper functions – use the one that fits the data type: e.g.sanitize_text_field()
for plain text,sanitize_email()
for email addresses,sanitize_file_name()
for file names, etc.. Sanitizing early ensures that you’re working with clean data internally. For HTML content, you may usewp_kses_post()
to allow only safe HTML tags, orwp_kses
with a custom whitelist. - Validate Data: Sanitization alone isn’t enough; you must also validate that the data meets your expectations. For example, if you expect a number, after sanitizing, ensure it’s numeric or cast to int. If only specific values are allowed, check against that whitelist. Validation prevents users (or attackers) from supplying unexpected values (like the word “dog” when a number is required). Always assume user input could be wrong or malicious. By validating, you avoid processing bad data.
- Specific Examples:
- If expecting an integer ID from
$_GET['post_id']
: useintval()
or cast to(int)
aftersanitizing, and perhaps confirm that post exists withget_post()
. - If expecting one of a few strings (e.g., a dropdown selection), check
if ( in_array( $value, ['option1','option2'], true ) ) { ... } else { /* invalid */ }
. - Use WordPress functions like
is_email()
for emails,validate_file()
for file paths, etc., where applicable.
- If expecting an integer ID from
- Don’t Over-Sanitize Global Arrays: Do not loop through
$_POST
/$_GET
and sanitize everything indiscriminately. Only process the specific inputs your plugin needs. Mass processing every request field is inefficient and can have unintended side effects. Target only the data you expect (e.g., your form fields or query parameters). - Sanitize Nonce Values: If you use
wp_verify_nonce()
orcheck_admin_referer()
, remember that nonce values are submitted via forms or URLs and need basic sanitization. Always
sanitize_text_field( wp_unslash( $_POST['your_nonce_field'] ) )
before passing it towp_verify_nonce()
. (Thewp_unslash()
accounts for WP slashing data, andsanitize_text_field
ensures no odd characters in the nonce.) This is a subtle requirement – the plugin reviewers often point it out becausewp_verify_nonce
itself does not sanitize input.
Output Escaping (Displaying Data Safely)
Complementary to sanitizing inputs, escaping outputs is critical before rendering any data to the browser. Escaping ensures that any content which might contain HTML or JS is neutralised (made harmless) when output, preventing Cross-Site Scripting (XSS) and layout breakage. WordPress provides a rich set of esc_*
functions for different contexts. Key principles and practices:
- Escape Late: Adopt the mantra “sanitize early, escape late.” In other words, clean data when received, but escape data at the moment you output it (not earlier). Escaping too early (e.g., storing escaped data in the database) is not recommended; data should be stored raw (but sanitized) and only escaped on output. This way, if you ever use the data in a different context (or output it differently later), you won’t have double-escaped issues, and you ensure maximum safety at output time.
- Use the Right Escaping Function: Choose the appropriate escaping function based on the output context. Common ones include:
esc_html()
– for escaping text to be output in HTML (normal context). Strips or encodes HTML tags.esc_attr()
– for escaping data to put inside an HTML attribute (e.g., value of an input, image alt text, etc.).esc_url()
– for escaping URLs before output in href/src.esc_js()
– for escaping a string to put inside an inline<script>
or onevent JS.esc_textarea()
– for content going into a<textarea>
.wp_kses_post()
– to allow a safe subset of HTML (like what’s allowed in post content). Use this if you want to output user-provided HTML (it will strip disallowed tags).wp_kses()
– to whitelist specific HTML tags/attributes if you have a specific set allowed.
Remember that even if you think data is safe (or came from WP itself), escaping is required. For example, options from the database that you sanitized on save should still be escaped on output – sanitizing makes data safe for storing and using internally, but escaping makes it safe to render in a browser. Never output raw data that came from users or external sources.
- Every Dynamic Variable Must Be Escaped: As a rule, whenever you
echo
or print a variable, it should be passed through an escaping function, unless you can absolutely guarantee it contains no HTML or malicious content. The plugin team specifically asks that “all $variables, options, and any generated data are escaped when output”. This includes data you might consider “safe” like numbers (you can still useabsint()
or cast to int for those), data from the database, and translated strings. - Translation Functions and Escaping: Be careful with WordPress translation functions:
__()
,_e()
,_x()
, etc. The plain__()
returns a translated string without escaping, and_e()
echoes a translated string directly. The review team often flags misuse of these:- If you use
__()
to retrieve a translation, wrap it in an escape likeesc_html( __('Hello','my-text-domain') )
. Or better, use the combined functionesc_html__()
, which does both in one call. Similarly, useesc_attr__()
if the string is for an HTML attribute. - Avoid
_e()
and_ex()
(which echo immediately without escaping). Instead, useesc_html_e()
oresc_attr_e()
, or echo an escaped__
as shown above. The goal is to ensure the final output is escaped, since translators could put potentially risky content (or at least content that breaks HTML) in the translations. Using theesc_html__
family makes it safe by default.
esc_url_raw
vsesc_url
: Note thatesc_url_raw()
is not for output escaping – it’s actually a sanitization function (for cleaning URLs before saving, for instance). When outputting a URL in HTML, useesc_url()
. (The naming can be confusing: think “raw” means use it on raw data to sanitize, whereasesc_url
is for escaping to output.)
- If you use
- JSON and AJAX responses: If you output JSON directly (say, in a REST API response or AJAX handler), you generally shouldn’t just
echo json_encode($data)
as-is. WordPress provideswp_json_encode()
which handles JSON encoding safely (and handles non-UTF-8 data). Usewp_json_encode()
for preparing JSON data. If you find yourself echoing a JSON string into an HTML context (e.g., embedding a JSON in a script tag), ensure it’s properly enclosed in<script>
and probably usewp_json_encode
for the data portion. (Note: If returning data viawp_send_json()
orwp_send_json_success()
in AJAX, WP will handle the encoding and content-type for you.) - Do NOT use
sanitize_*
functions for escaping: It’s a common confusion – sanitizing and escaping are related but distinct. Never assume a sanitization function will also safely escape output. For example, developers sometimes usesanitize_text_field()
at output time – this might strip some HTML, but it’s not its intended purpose, and if another plugin filters that function, it could fail to protect output. Always use the properesc_*
when echoing, even if the data was sanitized earlier. If needed, you can both sanitize and escape: e.g., when outputting a user-submitted string, you might doecho esc_html( sanitize_text_field( $string ) );
. The sanitize makes it safe for internal use/storage, the escape makes it safe for the browser. Both steps are important. - Allowing HTML (when necessary): Sometimes you want to output HTML that users or your code provided (like custom formatting). In those cases, using
esc_html()
would strip tags. Instead, usewp_kses_post()
to allow standard post HTML, orwp_kses($string, $allowed_tags_array)
with a custom allowlist. Never just output raw HTML from users without filtering it. The plugin team often cites misuse ofesc_html
on HTML content as an issue – if your plugin needs to output HTML (say, a custom field that allows links), you must use an appropriate kses function instead of a blanket escape that removes tags.
By consistently escaping output, you protect both your plugin’s users (from XSS) and yourself. Even if today you think “this variable can only be a number,” by escaping it you safeguard against future changes where that variable might contain something else. It’s a simple rule: any dynamic data that goes into HTML should be properly escaped at the point of output.
Database Queries and $wpdb (Preventing SQL Injection)
If your plugin interacts with the database directly (bypassing higher-level APIs), you must do so securely.

How to create secure WordPress compliant plugins
SQL injection is a serious vulnerability and a common reason for plugin rejection if found. The WordPress database access class $wpdb
provides methods to help prevent SQL injection.
Key guidelines:
- Use
$wpdb->prepare()
for dynamic queries: Never concatenate or interpolate user inputs directly into an SQL query string. Instead, use placeholders (%s
for strings,%d
for integers,%f
for floats) in your SQL and pass the user-provided values as additional arguments to$wpdb->prepare()
. For example:In the “GOOD” example, the
%s
placeholder is safely replaced with the sanitized username, properly quoted for SQL. The difference is thatprepare()
ensures special characters are escaped for SQL context, preventing an attacker from breaking out of the query. - Always validate data types: Even when using
prepare()
, ensure the data is of expected type (e.g., cast to int for numeric IDs). The placeholders inprepare
do some type handling (%d
will cast to int), but you should still validate ranges or allowed values if applicable (e.g., ensure a limit or offset is a positive integer). - Multiple Placeholders / IN Clauses: If you need to inject an array of values (e.g., a list of IDs for an
IN()
clause), you can’t directly pass an array toprepare
. You have to programmatically construct the right number of placeholders. For example, to safely prepare anWHERE id IN (...)
query for a dynamic array, you might do:This technique is demonstrated in the plugin handbook. It ensures each item is properly escaped. (There is a WP core ticket to improve this, but until then, manual placeholder generation is required.)
- Use
$wpdb
methods appropriately: Prefer$wpdb->get_results()
,$wpdb->get_var()
,$wpdb->insert()
,$wpdb->update()
, etc., which all can accept prepared queries or data arrays. These methods often handle some sanitization internally (for example,$wpdb->insert()
will escape values for you, though you should still sanitize data before passing it). - Avoid direct use of
mysqli_*
orPDO
in plugins: Always use$wpdb
(which uses the proper database abstraction under the hood). Reviewers will flag plugins that open their own database connections or use raw PHP database functions – that’s unnecessary and might break compatibility (like multi-site, etc.). - No arbitrary queries without need: If WordPress provides a function or API to get the data you need, use that instead of a direct query. For example, use
get_posts()
or WP_Query for fetching posts,get_option()
for options, etc. Direct SQL should be reserved for cases where no higher-level API exists or for complex operations where performance demands it.
By following these practices, you protect against SQL injection – which can be catastrophic if not handled. The Plugin Review Team explicitly checks for use of $wpdb->prepare()
on any query that incorporates user input. If they see something like ... where id = $_GET['id']
with no prepare, they will reject it outright. Always err on the side of caution: prepare every variable in queries, and escape like you’re protecting a bank vault.
Nonces and User Capabilities (Security Checks for Actions)
WordPress provides nonces (one-time tokens) and a robust capability system to help secure form submissions, AJAX actions, and any operation that changes site data. Proper use of nonces and capability checks is mandatory for any plugin that performs actions (especially those accessible via the front-end or by less-trusted users). Here’s how to implement them:
- Use Nonces for Form and URL Actions: A nonce is a number used once – essentially a hidden token that verifies the intent of a request came from a valid source (your site) and not an external malicious request. Whenever you create a form that modifies data (settings form, content submission, deletion links, etc.), include a nonce field. In PHP, use
wp_nonce_field('your_action_name', 'your_nonce_field_name')
to generate a hidden input with a nonce. For actions triggered via URL (e.g., a GET link that triggers deletion), you can usewp_nonce_url()
to attach a_wpnonce
parameter, or manually addnonce_field = wp_create_nonce('your_action_name')
to the link. - Verify Nonces on Processing: When your code receives a submission (in an admin page handler or an AJAX endpoint), always check the nonce before doing anything. In admin pages, use
check_admin_referer('your_action_name', 'your_nonce_field_name')
which will verify the nonce (and die with a message if it’s invalid). For custom form handling in front-end (if not using admin-post.php), you can usewp_verify_nonce()
manually. For AJAX actions, usecheck_ajax_referer('your_action_name')
in yourwp_ajax_...
handler. Nonce verification ensures the request is intentional and not CSRF (Cross-Site Request Forgery). If the nonce fails, your code should not proceed (and usually shouldwp_die()
or exit). - Nonce Naming: The first parameter of
wp_nonce_field()
andcheck_admin_referer()
is the “action” name – use a string unique to that action. Many use the plugin slug or an action description (e.g.,'myplugin_delete_item'
). The nonce field name (second parameter ofwp_nonce_field
) can be generic (default is_wpnonce
if you omit it) or custom like'myplugin_nonce'
. Just ensure you use the same name when checking. As noted earlier, sanitize the received nonce value withsanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) )
before checking. The plugin team will expect this. - Protecting Against Replay: Nonces in WordPress have a time-based life (24 hours by default) and also tie to a specific user/session. This is usually enough to prevent reuse, but if you’re performing a particularly sensitive action, you might further validate (e.g., use one-time tokens or additional verifications). For most cases, WordPress nonces are sufficient.
- Check User Capabilities: Before performing any action that changes data or accesses sensitive information, ensure the current user has the proper capability. WordPress’s
current_user_can()
function should be used liberally to gate functionality. Examples:- If your plugin adds an admin settings page, verify
current_user_can('manage_options')
(only admins can change site-wide settings) before saving those settings. - If you have a front-end action that deletes a post (as in an earlier code example), check
current_user_can('delete_post', $post_id)
or whichever capability is appropriate (maybeedit_post
if authors can delete their own posts) as well as a nonce. - For custom post types or any custom capability, use those in
current_user_can()
. E.g.,current_user_can('edit_awesome_data')
.
The role/capability system is granular; choose the lowest capability that makes sense. Common ones:
manage_options
(site admin),edit_posts
(authors+),edit_others_posts
(editors+),publish_posts
,unfiltered_html
(admins only, for posting raw HTML), etc. If unsure, using an admin-only cap likemanage_options
for admin pages is safe. - If your plugin adds an admin settings page, verify
- Don’t Rely on is_admin(): The function
is_admin()
does not check user role – it only checks if the current request is in the admin backend. It’s not a security check. Reviewers have flagged plugins that used onlyis_admin()
to guard sensitive actions, which is not sufficient. Always use capability checks;is_admin()
can be used in combination if needed (e.g., you only want code to run in the admin area anduser must be admin). - REST API Endpoints: If your plugin creates custom REST API routes (using
register_rest_route()
), you must provide apermissions_callback
that checks capabilities (or otherwise validates the request). Since WP 5.5, omittingpermissions_callback
triggers a warning by core. Do not just setpermissions_callback => '__return_true'
(which would make the endpoint accessible to anyone) unless the data truly should be public. If an endpoint changes site data or reveals private info, checkcurrent_user_can()
inside the callback appropriately. - Examples of combination: Suppose you have a form that lets an admin create a custom post entry from a settings page. When processing:
This way, even if someone managed to submit the form from outside, they’d need a valid nonce (protecting from CSRF) and to be an admin (protecting from lower-privilege misuse).
Implementing nonces and capability checks might feel repetitive, but it’s non-negotiable. Many plugin submissions are rejected for things like “missing nonce verification” or “insufficient permission checks” – for example, a plugin that registers an AJAX action but doesn’t call check_ajax_referer()
will be flagged as a security risk. Always think: “Should this user be allowed to do this action? Did this request actually come from my plugin’s UI?” – use capabilities and nonces to make sure the answer is “yes”.
Using WordPress APIs and Libraries (Don’t Reinvent the Wheel)
WordPress provides numerous APIs to handle common tasks (HTTP requests, file handling, cron scheduling, etc.). The plugin team expects you to use these instead of custom or direct PHP functions, both for consistency and security/stability:
- Remote HTTP Requests: If your plugin needs to fetch data from an external API or URL, use the WordPress HTTP API functions (
wp_remote_get()
,wp_remote_post()
, etc.) rather than cURL or file_get_contents. The HTTP API will automatically use the best method available (streams, cURL, etc.) and integrates with WordPress settings (it respects proxy settings, SSL, etc.). Usingcurl_*
functions in your own code is highly discouraged for plugins – the reviewers may ask you to replace it with the HTTP API. The only exception is if you include a third-party library that uses cURL internally; that’s acceptable, but your plugin’s code should not unless absolutely necessary. If you must set custom cURL options, the HTTP API provides hooks (likehttp_api_curl
) to tweak the request. - File Operations and Uploads: For handling file uploads, rely on WordPress’ media handling functions. If users upload files via your plugin, use the WP_File_Upload and media_handle_upload() or lower-level
wp_handle_upload()
rather thanmove_uploaded_file()
directly. WordPress’s functions will check file types, handle duplicates, and place files in the correct upload directories. The plugin guidelines explicitly forbid settingALLOW_UNFILTERED_UPLOADS
to true in your code
– that constant would allow any file type upload (security risk). Instead, if you need to allow an additional file type, use the proper filters (upload_mimes
) rather than bypassing checks. - Using Built-in Libraries: Do not bundle or include code for libraries that WordPress already includes (unless you have a compelling reason and handle conflicts). WordPress ships with many common libraries – jQuery, jQuery UI, React, Moment.js, SimplePie (RSS), PHPMailer, PHPass (password hashing), etc. The guideline is clear: “plugins may not include those libraries in their own code, but instead must use the versions packaged with WordPress.”. Including duplicate versions can introduce security issues and bloat. For example, if you need jQuery, use
wp_enqueue_script('jquery')
to load WordPress’s copy, rather than bundling your own. If you need a library that is not in core, you can include it, but only include what you need. (E.g., don’t include all of jQuery UI if you only need the datepicker – just enqueue thejquery-ui-datepicker
handle that comes with WP, or if not available, include that component alone.) Ensuring compatibility with WP’s libraries is important; if something doesn’t work with the built-in version, it’s often a conflict or “noConflict” issue that should be resolved, not a reason to force your own version. - Settings API and Options: When creating admin option pages, it’s recommended to use the WordPress Settings API (register settings, use
add_settings_section
,add_settings_field
, etc.) for robust handling of options pages, including nonce generation automatically. Even if you don’t use the full Settings API, useget_option()
andupdate_option()
to store config, rather than custom database tables for simple settings. Only create custom tables if you need to store a lot of structured data (and if you do, prefix the table names and maybe usedbDelta()
on activation to create them). - Cron Scheduling: For scheduled events, use WP-Cron (
wp_schedule_event()
) instead of attempting to call things via external cron jobs or timing loops. WP-Cron will ensure events fire when someone visits the site (though not exact to the second). - Transient and Cache API: For caching data, use the Transient API (
set_transient()
, etc.) or WP Object Cache functions (wp_cache_set
) so your plugin works nicely with WordPress caching mechanism.
Using WordPress APIs generally makes your plugin more stable and secure because these APIs handle many edge cases.
The Plugin Check tool and reviewers will flag direct uses of things like curl_init
or direct file writes as issues to be changed to WP functions. So leverage the platform – it will save you time and passes the compliance checks.
Enqueuing Scripts and Styles Properly
If your plugin needs CSS or JavaScript (which most do), you must add them the correct way. Injecting raw <script>
or <style>
tags or using improper hooks is a common mistake that leads to rejection. Follow these rules:
- Use
wp_enqueue_script()
andwp_enqueue_style()
: WordPress has a robust system for loading assets. Register or enqueue your scripts/styles using these functions. Do this in a hook, typicallywp_enqueue_scripts
(for front-end assets) oradmin_enqueue_scripts
(for assets on admin pages). Never directly echo script or link tags in your plugin’s HTML output. For example:This ensures WordPress includes them at the right time, avoids duplicate loads, and lets users (or other plugins) manage dependencies.
- Don’t Hard-Code Assets in HTML: For front-end, do not print
<script src="...">
in your templates directly. Similarly, do not use file system paths for assets; always useplugins_url()
orplugin_dir_url(__FILE__)
to get the correct URL (some setups movewp-content
around, so never assume/wp-content/plugins/...
). The above example withplugin_dir_url(__FILE__)
shows how to build a URL to your plugin’s asset. - Register first if needed: If you have multiple scripts or dependencies, you can
wp_register_script()
(with a handle and dependencies) and thenwp_enqueue_script()
that handle. Registering is optional, but can help if you need to attach data viawp_localize_script
or want to ensure a script is loaded only under certain conditions. - Inline JS/CSS: If you need to add a small inline script (for passing dynamic data or initialising something), use
wp_add_inline_script( $handle, $js_code )
after enqueuing a script. For CSS, usewp_add_inline_style( $handle, $css_code)
. This attaches your inline code in the proper place, rather than echoing it directly in a page. Keep inline scripts minimal and primarily for data output (e.g., printing avar MyPluginData = {...}
object that your enqueued script can use). - Conditional Loading: Don’t load your assets everywhere if they’re only needed on specific admin pages or front-end pages. For example, if your script is only used on a custom post type edit screen, check the
$hook
or the current page before enqueuing. Overloading every page with your plugin’s JS/CSS is bad practice. The plugin team likes to see that you enqueue assets only when needed. - No CDN dependencies without consent: If your plugin relies on an external CDN (like for a library), be cautious. Guideline #8 disallows loading executable code from third-party servers without explicit need. It’s better to bundle the library or use WP’s version. E.g., don’t include a
<script src="https://cdn.jquery.com/...">
– use WP’s jQuery. For fonts or non-executable assets, it’s a gray area (loading Google Fonts is usually okay, but be mindful of GDPR/privacy if doing so, and consider offering a local alternative). The safest route is to not depend on external resources in the plugin. If you do, document it or allow opting out.
By correctly enqueuing, you ensure compatibility with other plugins and themes. Improper inclusion (like multiple jQuery loads) can cause conflicts – the review team’s common issues list explicitly mentions to use the built-in wp_enqueue commands for JS/CSS.
Internationalization (Making Plugin Translatable)
All plugins in the WordPress.org repository are expected to be ready for translation. This is not just about complying with guidelines but also expanding your user base.

Understanding Internationalization (i18n)
Key points for i18n:
- Text Domain: As mentioned, your plugin should declare a Text Domain in the header that matches the plugin’s slug. E.g., if your plugin slug (folder name) is
my-great-plugin
, your header should haveText Domain: my-great-plugin
. Additionally, load the text domain in your code (in most cases WordPress will automatically loadplugins_dir/languages/my-great-plugin-en_US.mo
if it exists, but it’s good practice to callload_plugin_textdomain('my-great-plugin', false, basename(dirname(__FILE__)).'/languages');
in an init hook). Ensure any translation files are placed in a/languages
folder. - Wrapping All User-Facing Text: Any string that is output to the user (in the front-end or admin screens) should be wrapped in translation functions. Use
__('string','text-domain')
for strings that you will later echo (escaped appropriately!), or_e('string','text-domain')
if you absolutely need to echo directly (though as noted, prefer the escaped versions). For strings with variables, usesprintf
/printf
or use__()
with placeholders:Note the use of translators comments (
/* translators: %s: user name */
in complex cases) to clarify meaning for translation teams. - No Dynamic or Concatenated Text in Translatable Strings: Do not build sentences by concatenating pieces or inserting variables without placeholders. For example, don’t do
__('Hello, ','text-domain') . $name . __('!','text-domain')
. Instead, do as above with one string__('Hello, %s!','text-domain')
and a placeholder. Similarly, do not use variables as the text domain or as the text string in translation functions. The parser that generates translation templates needs literal strings. For instance, this is wrong:It should be a fixed string key. If you have a dynamic message, consider using contexts or multiple known strings. Likewise, don’t do
__($string, $some_domain)
– the domain must be a literal. The guideline is: “do not use variables or defines as text, context or text domain parameters of any gettext function – they NEED to be strings.”. Violating this means your plugin’s strings won’t actually be translatable. - Follow Text Domain and Domain Path rules: The text domain is typically your slug; ensure it’s consistent everywhere. If your plugin slug changes (likely it won’t, once submitted), that must change. If using a
languages
folder, mentionDomain Path: /languages
in the plugin header, so WordPress knows where to find translation files. - Example:
This ensures “You have %s new items” is what translators see, and they translate the whole phrase (perhaps reordering if needed for their language).
A plugin not being translation-ready isn’t typically a hard rejection reason, but it’s strongly encouraged, and users notice. Moreover, the Plugin Check tool flags usage of __()
without proper escaping, or variables in translation calls, as warnings.
So to pass automated checks and future-proof your plugin, implement i18n best practices from the start.
Other WordPress Plugin Security & Quality Considerations
In addition to the above major areas, ensure the following to meet coding standards and avoid common pitfalls:
- No Eval or Obfuscated Code: Do not include any eval() or create_function hacks, and absolutely no obfuscated or minified code without source. The repository rules explicitly forbid hiding code or using encryption/packing (like
p,a,c,k,e,r
or similar). If you minify your plugin’s JS/CSS, you must include the unminified source or provide a link to a public repository where it can be obtained. The goal is all code in the plugin should be human-readable. So, avoid minifying your PHP (never do that), and if you use build tools for JS, include a link to your GitHub, for example. Also avoid unnecessarily compressed data in the plugin; any binary or encoded blobs will raise suspicions. - No AJAX or Form actions without
die()
: When handling an AJAX action (viaadmin-ajax.php
), after outputting the response (like usingwp_send_json
or echoing something), callwp_die()
or simply ensure the script exits. WordPress’s AJAX handler usually dies for you after your callback finishes, but if you’ve started output buffering or something, be cautious. Similarly, after processing a form that does a redirect, callexit;
afterwp_safe_redirect()
to stop execution. These are minor things but can prevent unexpected additional output. - Error Handling: Use WordPress functions for error logging or displaying admin notices rather than echoing PHP errors. Never leave
var_dump
orprint_r
calls in production code. If you catch exceptions or errors, handle them gracefully (maybeerror_log()
or admin_notice). Also, avoid using PHP’serror_reporting()
orini_set('display_errors')
in your plugin – plugins shouldn’t alter global PHP settings, as per guidelines. If you need to adjust execution (like unlimited memory or no time limit) for a specific operation, do it temporarily and revert if possible, but don’t set it globally on plugin load. - PHP Compatibility: Check the minimum PHP version your plugin requires (and declare
Requires PHP
in the header). Don’t use short open tags<?
(use<?php
) – short tags are not guaranteed to be supported and are disallowed by WP coding standards. Also avoid using features not available in your minimum PHP version. The Plugin Check tool or reviewers might run your code in an environment to confirm it doesn’t fatal error on older supported versions. - No Global State Changes: Don’t change global WordPress settings/environment. For example, do not call
date_default_timezone_set()
– WordPress expects the timezone to remain UTC internally. Changing it can break core functions. Similarly, don’t override$wpdb
globals or other core singletons. Keep your plugin self-contained. - Admin Dashboard Etiquette: If your plugin adds admin notices or dashboard widgets, ensure they are dismissible and not overly intrusive. Guideline #11 warns against “hijacking” the admin dashboard with constant nags or ads. It’s fine to notify about important updates or promotions, but do it in a limited, user-friendly way (e.g., only on your plugin’s settings page, or as a dismissible notice that doesn’t reappear constantly). A common community complaint (and possible reason for bad reviews or even rejection if extreme) is plugins that spam the admin with large banners or notifications.
- Opt-in Tracking Only: If your plugin wants to collect any data (usage tracking, admin email, etc.), it must be opt-in (user explicitly agrees) as per guideline #7. This includes even innocuous tracking like hitting your server to check for updates or sending plugin usage stats. By default, do not send anything home. And if you do after opt-in, disclose it in your documentation/privacy policy. Plugins that phone home without consent are often rejected or pulled.
- No Unauthorised Changes: A plugin should never, for example, auto-activate other plugins, or deactivate them, or modify core behavior, without user action. Guideline #13 forbids plugins from arbitrarily modifying other plugins’ active status
(with an exception: if your plugin depends on another plugin, you can deactivate your plugin if dependency is missing, which is logical). Also, do not include automatic update code that bypasses WordPress.org. Some plugins include custom update checkers (for off-dotorg updates); if your plugin is accepted on .org, you must remove any external update checking code. The .org system will handle updates. The plugin team disallows plugins that phone home for updates or manipulate the updater because it conflicts with the official system. - GPL Compliance: All code and assets in your plugin must be GPL-compatible. Do not include third-party libraries that are proprietary or have non-GPL-compatible licenses. If you’re unsure, consult the list of GPL-compatible licenses. Also, no “premium” code that’s not supposed to be freely shared. For example, don’t include a copy of a library that’s only for premium use (unless that library allows it under GPL). If a reviewer finds non-GPL code, they will require you to remove it. Likewise, if you have minified assets, include a way to get the source (link or include). Basically, your plugin should embrace open-source spirit fully.
By covering all these bases in development, you’ll have a high-quality, secure plugin. Security issues and blatant guideline violations are the top reasons for rejections, so double-check everything.
In the next section, we’ll enumerate common rejection reasons and how to avoid them, which will reinforce many points covered here.
Common Plugin Rejection Reasons & How to Avoid Them
Even well-meaning developers sometimes run afoul of the Plugin Directory rules, resulting in their plugin being rejected or needing revisions.

Common reasons for plugin rejection on WordPress.org
Below is a list of common rejection reasons reported by the plugin review team, forums, and developer communities, along with tips to mitigate each. By checking these before submission, you can save a lot of time:
- Missing or Improper Data Sanitization/Escaping: Perhaps the most frequent issue. If you fail to sanitize inputs or escape outputs, reviewers will flag it. For example, directly using
$_POST
values in SQL or echoing user input to a page withoutesc_html
. Mitigation: Audit every instance of input/output in your code. Usesanitize_text_field()
,intval()
, etc. on all inputs, and wrap every dynamic output withesc_html()
,esc_attr()
, etc. Remember: sanitize on input, escape on output. Double-check places like admin notices, front-end outputs, etc., for any unescaped variables. - Unsafe Database Calls (SQL Injection Vulnerabilities): If you build SQL queries with user data directly, that’s a red flag. Reviewers often search for
$wpdb->get_results(
and see ifprepare()
is used. Mitigation: Always use$wpdb->prepare()
for queries with placeholders for any variable parts. Or use safer API functions. Test your queries with something like an apostrophe in inputs to ensure they won’t break. If the plugin team sees code like$wpdb->query("DELETE FROM table WHERE id=$id");
(with$id
not prepared), they’ll reject and point you to fix it. Use prepared statements and appropriate data types everywhere. - No Permission Checks or Nonce Checks: If your plugin performs actions (especially deletion or editing of data) without verifying the user’s capability or a nonce, it’s considered a security issue. Mitigation:Ensure every form or action that changes something calls
check_admin_referer()
orcheck_ajax_referer()
as appropriate, and thencurrent_user_can()
for relevant caps. For instance, if any user could trigger an AJAX to delete content and you didn’t lock it down, that’s a problem. Walk through each action and confirm “Should the user be logged in? What role? Did I verify intent (nonce)?” If not, add those checks. - Direct File Access (No ABSPATH check): Plugins that don’t protect against direct access may be rejected. This is easily caught by reviewers by simply trying to open a plugin PHP file in a browser or scanning for the
ABSPATH
constant usage. Mitigation: Add theif ( ! defined('ABSPATH') ) exit;
line at the top of any PHP file that could be loaded directly (basically all your PHP files except maybe ones that only define classes/functions). It’s a one-liner that can save you from this rejection reason. The StackExchange example showed a plugin rejected for allowing direct access to a file and trying to manually includewp-load.php
– adding a proper hook and check resolved it. - Hard-Coding Paths or URLs: Assuming a plugin path like
/wp-content/plugins/plugin-name/...
is a mistake (some installs relocate wp-content). Reviewers might run WordPress in an environment with custom paths to catch this. Mitigation: Useplugins_url()
,plugin_dir_path()
,content_url()
, etc. WordPress makes these easy – any hardcoded reference towp-content/
or similar should be replaced. Also, avoid assuming the site URL; use APIs to get paths. - Including WordPress Core Files or Using Wrong Loading Methods: As noted, calling
require wp-load.php
is a known instant rejection reason. Mitigation: Use hooks for everything. If you need WP functionality, your code should be running inside WP, not bootstrapping it. For background tasks, use WP Cron or CLI, not direct includes. - Improper Use of Hooks (Especially on Initialization): This is more subtle: e.g., doing heavy work on every page load rather than only when needed. Not usually a “rejection” reason per se, but extremely inefficient code might get flagged in review comments. Mitigation: Hook expensive operations to specific admin pages or conditions. For example, don’t fetch external data on every page load; maybe fetch on a schedule or on-demand. Use
init
or later hooks to ensure WP is loaded for your functionality. - Not Using the Settings API or Malforming the Admin UI: If your plugin creates an admin settings page that doesn’t use WordPress styles or has fields that don’t sanitize/validate input, it might be pointed out. Mitigation: Follow WP UI patterns. Use
add_options_page()
oradd_menu_page()
to create pages, use WP form nonce, table classes, etc. While the plugin team won’t reject solely for UI aesthetics, a very inconsistent or broken admin UI might raise an eyebrow. - Loading Assets Incorrectly: For example, printing scripts directly, or not using
wp_enqueue_script
, can lead to a request to change it. Mitigation: Use the recommended enqueuing methods always. Remove any inline<script>
tags that output file references or large code. If you must output a small inline script, tie it to an enqueued handle withwp_add_inline_script
. This also prevents conflicts (like double-loading jQuery etc.). Many rejections happen because a plugin included a second copy of jQuery or another library – which is against guideline #13. Always use WP’s built-in copy to avoid this problem. - Using Disallowed Functions or Syntax: Examples: using PHP short tags (
<?
), usingeval()
, usingbase64_encode/decode
for obfuscation (this looks very suspicious). Mitigation: Use full<?php ?>
tags, remove any eval logic (if you needed dynamic code, there’s almost always a safer design). Don’t include encoded blobs of data – if you need to include an image or something, use an actual file, not a base64 string in PHP. These things trigger security scanners and reviewer scrutiny. - Excessive External Calls or Tracking: If the plugin contacts third-party servers on its own (for updates, tracking, etc.) without user knowledge, it’s likely to be flagged. This could even include loading Google Fonts or hitting your own API. Mitigation: Keep external calls to a minimum and document them. For updates, rely on WP.org (remove custom update checks). For analytics/tracking, have an opt-in setting. If you must fetch remote data (like from an API), ensure it’s necessary and use proper timeouts and caching.
- Undisclosed Use of a Service: Related to above, if your plugin requires an external service (like connecting to a SaaS API), you must disclose it clearly. The plugin team often asks developers to clearly state in the readme that “This plugin connects to XYZ service to function” and include links to that service’s terms/privacy. Mitigation: Add a section in your readme (and perhaps in the plugin’s admin UI) explaining any external service and what data is sent. Provide links to the service and privacy policy. This is for transparency to users and legal protection.
- Including Unneeded Files/Folders: Reviewers often see plugins submitted with development cruft: full node_modules directories, Gulp configs, .git directories, etc. These bloat the plugin and aren’t needed by end users. Mitigation: Clean your plugin package before submission. Remove any dev-only files (except those you plan to intentionally include like a composer.json for reference). Unit tests, if included, are not typically needed in the distributed plugin – you can remove them or put them in a separate branch. The repository should mainly contain what’s needed to run the plugin. (It’s fine to link to your GitHub for those who want to see tests or unminified code, as long as you disclose where to find the source.)
- License or Copyright Issues: Using “GPL-compatible” is a must. If you include third-party code, ensure its license is compatible (MIT, BSD, etc., are okay; a proprietary or unclear license is not). Also, using trademarks inappropriately (like naming your plugin “Facebook Something” when you’re not Facebook) can cause denial due to trademark policy. Mitigation: Rename any problematic usage (e.g., “WP FaceBook Widget” might be asked to change if Facebook complains – better to say “Social Media Widget for Facebook”). Also, double-check that any included libraries have license info. If you use something like a web API SDK, include its license file or credit. The plugin team may ask you to remove non-GPL code entirely.
- Incorrect Readme or Metadata: Sometimes a plugin isn’t rejected for this, but a common issue is mismatched versions (plugin header says 1.0, readme says 1.1) or wrong “Tested up to” values (like a future version or a very old one). Mitigation: Always update your version in both the main file and readme. Use the official readme template. Ensure Stable Tag is correct (pointing to a tag that exists). If these don’t match, the Plugin Check tool will block submission until fixed.
In summary, most rejections boil down to security issues, guideline violations (like phoning home or spammy behavior), and incomplete adherence to WordPress standards.
The Plugin Review Team even published a list of common issues they correct in 95% of plugins – which maps to many points above: sanitization, escaping, prefixing, no direct file access, using WP’s libraries and APIs, etc.
Use the checklist (provided next) to review your plugin before submission. If you address all those points, you stand a very good chance of a smooth review and approval.
Submission & Release (SVN) Steps
Once your plugin code is ready and compliant, you’ll move on to the submission and release stage

Managing plugins with SVN & Version Control for WordPress.Org
This involves submitting your plugin for review, and upon approval, using Subversion (SVN) to manage your plugin’s repository on WordPress.org. This section guides you through those steps:
Before Submission: Final Prep
- Test with Plugin Check: As noted earlier, WordPress now requires new plugin submissions to pass a pre-screening by the Plugin Check tool. It’s a plugin (and also run server-side on submission) that checks for common errors. It will catch things like: PHP syntax errors, usage of forbidden functions, missing text-domain, incorrect escaping, version mismatches, etc. Install and run Plugin Check on your plugin locally; fix any issues it reports. Keep in mind Plugin Check is evolving – it’s meant to help, but if it flags something you believe is a false positive, you can note it in your submission (though generally, try to satisfy it to avoid delays).
- Ensure 2FA is enabled: As of Oct 2024, all plugin authors and committers must have Two-Factor Authentication on their WordPress.org accounts to submit a new plugin. Make sure you set up 2FA (e.g., via the WordPress.org profile security settings with an app or email confirmation) before you attempt to submit. If you don’t, your submission may be blocked.
- Preparing the ZIP: You will submit a ZIP file of your plugin. This ZIP should contain your plugin folder with all necessary files (and ideally no unnecessary ones as discussed). Double-check that you haven’t left any
.git/
folders, hidden files (except maybe a.htaccess
if needed for security in a folder, though that’s rare), or large unused assets. The ZIP name isn’t too important (the system will use the Plugin Name and slug you provide separately), but naming it after your plugin or plugin-version is fine. - Fill in Submission Details: On the “Add Your Plugin” page (wordpress.org/plugins/add/), you’ll need to provide: Plugin Name, Description, Plugin URL (if any, can be your website or GitHub), and the ZIP file. The name and description here should match or align with your plugin’s internal readme and header info. Mention if your plugin relies on an external service or has any special requirements in the description.
- Review Guidelines One More Time: The submission page will remind you of guidelines. It’s wise to skim the official Detailed Plugin Guidelinesonce more to ensure compliance (we covered them, but e.g., if your plugin had any “Powered by” credit links on front-end, ensure it’s opt-in, etc.).
Submitting the Plugin (Initial Submission)
- Submit and Acknowledge Emails: When you hit submit, you should see a confirmation that your plugin was submitted. Shortly after, you’ll receive an email from WordPress Plugin Repository ([email protected]) confirming the submission. Keep an eye on your email (and spam folder) in the coming days. The plugin review team may contact you either to ask for changes or to inform you of approval. Make sure your email on your WordPress.org profile is up-to-date
and that emails from WordPress.org are not going to spam. If they ask questions or request changes, respond promptly and courteously. Not responding can result in your plugin being closed without further notice. - Waiting Period: As of recent reports, new plugin reviews can take anywhere from a few days to a couple of weeks (depending on backlog). The queue was large in 2023 (several hundred plugins, with ~90 day wait), but efforts have been made to reduce it. Using Plugin Check and having a clean plugin can speed up the process since you won’t have to resubmit fixes. Be patient – do not repeatedly ask the status on forums; it’s okay to politely inquire if it’s been, say, a month with no word, but usually you’ll hear back once a reviewer picks it up.
- Potential Outcomes: The reviewer may:
- Approve the plugin directly (you’ll get a “Congratulations, your plugin is approved” email with SVN instructions).
- Respond with a list of issues to fix before approval (common for first-time submissions). They’ll typically quote the guideline or error (e.g., “Please sanitize POST data on line X
” or “Remove the third-party library Y as it’s not allowed under GPL”). You can then fix and reply back with a new ZIP or inform them that it’s fixed (often they’ll allow you to attach the new ZIP in an email reply). - In some cases, reject the plugin outright (this is rare unless the plugin fundamentally violates a guideline, like it’s something disallowed). More often they give you a chance to adjust.
- Tip: If you get feedback, implement it diligently. The plugin team’s goal is to help your plugin be safe and compliant. They will re-review your changes and then approve if all is well.
When approved, your plugin gets its own SVN repository on WordPress.org. If you’ve not used SVN, it’s a version control like Git. You don’t need deep SVN knowledge; just know the key parts of the repo:
- Trunk: This is where the latest (development) version of your plugin resides. Think of
trunk
as the bleeding edge or the next release you’re working on. When you first get SVN access, you can upload your current code intotrunk
. Many developers treattrunk
as the current stable version during development until they tag a new release. - Tags: This directory contains sub-folders for each released version. For example, when you release version 1.0.0, you create a
tags/1.0.0/
directory and put the code for that version there (usually by copying it from trunk). Each tag should be named exactly as the version (match your plugin’s Version in the header). Do not modify tagged versions once created (except to remove security issues in extreme cases); they are snapshots for reference. Users download releases from these tag folders when they update or install specific versions. - Branches: Not typically used by most plugin authors unless you maintain multiple major versions. You can ignore this unless you plan advanced usage.
- Assets: There is a separate assets folder outside trunk/branches/tags at the same level. This is for plugin directory assets (like the banner image, icon, screenshots). The contents of
/assets
are used by the wordpress.org plugin page for your plugin. For example, you can putbanner-1544x500.png
,icon-256x256.png
, and screenshot images likescreenshot-1.png
, etc., there. These are not included in the plugin download (they’re only for display on the repo page). The “readme.txt” will reference screenshots by their names. The initial approval email usually links to the asset guidelines if you want a banner or icon.
When you get the email, follow the instructions to checkout your SVN repository (it will have a link something like svn checkout https://plugins.svn.wordpress.org/your-plugin-slug/
).
You’ll need an SVN client (you can use command-line SVN or tools like TortoiseSVN on Windows, SmartSVN on Mac, etc.).
Use your WordPress.org username and password (note: if your password has special chars, SVN might have issues; you can also use an application password from your profile).
Importing Your Plugin into SVN
- Once you have the repo checked out, you’ll see an empty directory with the folders:
trunk
,branches
,tags
(and anassets
if it was auto-created; if not, you can create anassets
directory manually). - Add files to trunk: Copy your plugin files (PHP, readme.txt, asset folders like css/js within your plugin) into the
trunk
directory. Ensure thetrunk
now contains exactly what your ZIP had (except the readme will go here rather than somewhere else – some devs mistakenly put readme in the top, but yes trunk is fine). Thetrunk
should include the main plugin PHP file, all included files, and the readme.txt. - Add and Commit: SVN requires you to mark new files for addition (
svn add filename
for each new file, or recursive add). Thensvn commit -m "Initial import"
to upload. The initial commit goes to the repository. At this point, if you’ve set “Stable Tag” in readme.txt to a number (say 1.0.0) but you haven’t created that tag yet, the plugin page won’t show a download until you do the tag. So the next step: - Tag the first version: To tag version 1.0.0, for example, you can use SVN copy:
svn cp trunk tags/1.0.0
. This duplicates trunk to a new folder in tags. Then dosvn commit -m "Tagging version 1.0.0"
to push the tag. Alternatively, some people prepare the tag folder manually: copy files intotags/1.0.0
locally, then add and commit. Both achieve the same result. - Set Stable Tag correctly: In your
trunk/readme.txt
, the Stable Tag: field should read1.0.0
(whatever version you are tagging). Also, in thetags/1.0.0/readme.txt
, ensure it says the same stable tag (and version in main plugin file should be 1.0.0). If stable tag is set and tag exists, the system will use the readme from the tag folder for display. Make sure you commit the readme changes as well. - After tagging and pushing, within a few minutes, your plugin should appear on the WordPress.org plugin directory, listed as version 1.0.0. You’ll have a public page (wordpress.org/plugins/your-plugin-slug) where the description, screenshots, and download links are visible. Check this page to ensure everything looks right (sometimes formatting in readme or screenshot links might need tweaks).
Note: If you prefer not to use tags for some reason, the repository allows using trunk as the stable version by setting Stable Tag: trunk. However, this is not recommended for new plugins – and the plugin team actively discourages it now. It can confuse updates. So it’s best to always tag releases.
Readme.txt and Plugin Metadata in SVN
Ensure your readme.txt is properly formatted and included in both trunk and each tag. The readme is crucial for users to understand the plugin and for your plugin’s listing. A few important fields and sections in readme:
- Stable Tag: As discussed, this controls which tag is considered the released version. Keep it updated.
- Requires at least / Tested up to / Requires PHP: These fields are parsed from either the readme or plugin header. Update “Tested up to” whenever you test with a new WP release (so users aren’t scared off by an “untested” notice). These should be just version numbers (e.g., “Requires at least: 5.2” not “WP 5.2”).
- Short Description: The first paragraph under the plugin name in readme is used as the snippet below your plugin title. Keep it under ~150 chars. This should match the description you gave at submission, if possible.
- Screenshots: If you listed screenshots in readme (e.g., “1. Screenshot description”), ensure you have
screenshot-1.png
etc. in theassets
directory. And vice versa, don’t leave orphan images. - FAQ / Installation / Changelog: These sections are optional but recommended. “Installation” can be simple if it’s just “Upload and activate”. “Changelog” is good to update with each version changes (and required if you ever submit a security fix – users appreciate transparency).
- Upgrade Notice: You can include an “Upgrade Notice” for particularly important version updates (this shows in the updater if the version is major).
The readme is parsed by the directory to generate your plugin page, so any formatting issues can usually be fixed by adjusting markdown and committing again.
Use Markdown syntax sparingly and as documented (lists, headings, etc., but not all GitHub-flavored markdown works; see WP’s documentation for supported formatting).
Releasing New Versions of Your Plugin

Understanding the WordPress plugin approval process
When you’re ready to release an update:
- Increment the Version in your main plugin file (and typically also in readme “Stable Tag” if using trunk to tag flow).
- If adding a tag, update the Stable Tag in trunk readme to the new version number.
- Commit changes to
trunk
(this holds the new code). - Then create a new tag folder (e.g.,
svn cp trunk tags/1.1.0
, then commit). - Update the Changelog section in readme to reflect changes.
- Test that the new tag’s readme is consistent and that the plugin page reflects the correct latest version.
Users will automatically get update notifications since the readme and plugin header indicate a new version. The WordPress.org system checks the main plugin file’s Version against what they have installed to trigger updates, and serves the zip from the tag.
Important: Always increase the version number when you release. If you commit to trunk but don’t change the version, users won’t know to update (and guideline 15 requires each release to have a new version number). Also, do not reuse version numbers.
During/After Review – What to Expect
- Initial Delay: After you commit and tag the first version, it can take ~15 minutes for the plugin to be fully live (the directory caches flush periodically). If something doesn’t show up (like screenshots), give it a little time or check if your filenames/formats are correct.
- Feedback Loop: Sometimes, even after approval, the plugin team or users might report an issue (security or guideline) that was missed. If that happens, your plugin could be temporarily closed (you’ll get an email). Don’t panic – address the issue quickly and reply as instructed to get it reopened. For example, if a security flaw is found later, the team might close it until you fix it.
- Support Forum: Each plugin gets a support forum on WordPress.org. Be prepared to monitor it, especially early on. Timely, polite responses to user questions or issues will help your plugin’s reputation.
- Updates and Email Confirmation: For future updates, especially if your plugin becomes popular, WordPress may send a “Release Confirmation” email to you when you tag a new version, which you must confirm (this is to prevent compromised accounts from auto-pushing malicious updates). As a new plugin, you might not see this until you have a certain active install count.
- Plugin Stats and Growth: Once live, your plugin page will show download stats, etc. Make sure your readme is compelling and clear to attract users. Avoid any marketing speak that could be viewed as “spam” or irrelevant tags (guideline #12 forbids spammy tags and content in the readme). Use up to 5 relevant tags.
By following this guide from development through submission and leveraging the checklist below, you maximise your chances of a smooth launch on the WordPress.org repository.
The key is attention to detail and adherence to WordPress conventions – the plugin review team is there to uphold these for the benefit of users, and by extension, following them makes your plugin better.
How to Publish a Plugin to WordPress Using AI: A Developer Markdown Checklist
Below is a condensed Markdown checklist of steps and best practices for successfully creating a WordPress plugin that meets WordPress.org repository standards.

How to Publish a Plugin to WordPress Successfully
You can use this as a to-do list to verify your plugin’s compliance before submission or as a tool to assist AI agents to develop a WordPress plugin.
Each item is phrased as an actionable requirement or recommendation.
- Plugin Naming and Slug: Choose a unique plugin slug (folder name) and plugin name. Avoid using “WordPress” or trademarked names in the slug; if using a brand name (e.g., Facebook API integration), do not start with it (use “My Plugin for Facebook” instead of just “Facebook Plugin”). Ensure the Text Domain matches the slug exactly.
- File Headers: In the main plugin PHP file, include a standard header with at least
Plugin Name
,Description
,Version
,Author
,License
, andText Domain
. Example:The Version in the header must be updated for each release (no reusing version numbers). The Requires at least (WP version) and Requires PHP should be accurate.
- Licensing: All code, libraries, and assets included must be GPL-compatible. If you included third-party code, verify its license. Include license headers or a
LICENSE
file if appropriate. No encrypted or obfuscated code – all code must be human-readable. - Prevent Direct Access: Add an
ABSPATH
check at the top of every PHP file that executes code (other than purely definitional files). For example:This prevents direct hits to plugin files.
- Unique Prefixes: Prefix all function names, classes, hooks, and global variables with a unique namespace (often based on your plugin slug). Do not use common words or
wp_
/__
prefixes reserved for core. E.g., usemyplugin_
for functions (myplugin_init()
), and class names likeMyPlugin_Class
. - No Function Name Collisions: Avoid unconditionally defining functions or classes that might already exist. Relying on
function_exists
to skip definitions is not ideal except for polyfills/shared libraries. Your unique prefix is the primary means of collision avoidance. - Hooks and Core File Usage: Hook plugin functionality into WordPress actions/filters; do not manually include or require core WP files like
wp-load.php
. Useadd_action
/add_filter
for initialization (e.g., hookinit
oradmin_init
) rather than running code on file load. For AJAX, usewp_ajax_*
hooks or the REST API instead of custom entry points. - Admin Pages and Menus: If creating admin menu pages, use
add_menu_page()
/add_submenu_page()
appropriately. Only show menu items to users with proper capability (e.g.,'manage_options'
for a settings page). Do not hijack the dashboard with excessive top-level menus or notices. - Input Sanitization: Sanitize all external input (GET, POST, REQUEST, cookies, etc.) immediately when received:
- Use
sanitize_text_field()
,esc_url_raw()
,intval()
, etc., depending on context. - For rich text or HTML inputs, use
wp_kses()
orwp_kses_post()
to allow safe HTML tags. - Never trust user input – even admin users. Validate values (e.g., ensure numeric strings are numeric, options are in an allowed list).
- Sanitize WordPress-specific data: use APIs like
email_exists()
to validate emails, etc., where possible.
- Use
- Output Escaping: Escape all outputs that include any dynamic data:
- Use
esc_html()
for plain text in HTML context,esc_attr()
for attribute values,esc_url()
for URLs, and so on. - Escape translated strings too (prefer functions like
esc_html__()
andesc_html_e()
or wrap__()
inesc_html()
when echoing). - Apply escaping at the last moment before output (“escape late”). Do not store escaped data in the database.
- Even data coming from the database (options, post meta) should be escaped on output – assume it could have unsafe content.
- Double-check that no
echo
orprint
statements output a variable without an escaping function. This includes admin notices, form fields, widget outputs, etc.
- Use
- Prepared SQL Statements: Use
$wpdb->prepare()
for any direct database query that includes variables. For example:Do not concatenate user input into SQL strings. If using
prepare()
with multiple placeholders or arrays, construct placeholders safely (see WP documentation for%d
vs%s
, and how to handle IN() lists). - Use WordPress APIs: Wherever possible, use the WordPress-provided APIs instead of custom code:
- Use Settings API for settings pages (for built-in sanitization and nonce handling).
- Use Options API (
get_option
,update_option
) for storing settings, or custom post types/taxonomies for content, instead of custom database tables (unless truly needed). - Use WP Filesystem if writing files (especially on hosted environments) – typically not needed unless your plugin modifies files.
- Use HTTP API (
wp_remote_get
, etc.) for remote requests, not cURL directly. - Use wp_schedule_event() for scheduling tasks (instead of custom cron via OS or endless loops).
- Use User roles and capabilities (e.g.,
add_cap
,current_user_can
) instead of hardcoding user IDs or assumptions.
- Enqueue Scripts and Styles: Load JS and CSS via
wp_enqueue_script()
andwp_enqueue_style()
:- Register/enqueue in the appropriate hooks (
wp_enqueue_scripts
for front-end,admin_enqueue_scripts
for admin pages). - Use
plugins_url()
orplugin_dir_url(__FILE__)
to get asset URLs, not hard-coded paths. - Specify dependencies (like jQuery) in
wp_enqueue_script
to avoid duplicate loads. - If adding inline scripts or styles, use
wp_add_inline_script()
/wp_add_inline_style()
rather than echoing code in PHP. - Do not include your own copy of a core library (like jQuery, React, etc.) – use the one provided with WordPress.
- For conditionally loaded assets (only on certain pages), wrap your enqueue calls in appropriate checks (like
if ( $hook === 'settings_page_myplugin' )
for admin oris_singular('post_type')
for front).
- Register/enqueue in the appropriate hooks (
- No Remote Resources without Consent: The plugin should not pull content or scripts from external sites on its own (except where necessary and disclosed):
- No including remote JS/CSS files (e.g., from CDNs) unless absolutely needed, and note that in readme. Generally, bundle assets or use WP’s versions.
- No tracking or telemetry “phone home” without explicit opt-in from the user. If opting in, explain what data is sent.
- If plugin connects to a third-party API/service, disclose it in the readme (e.g., “This plugin connects to the XYZ API to retrieve data”) and provide links to that service’s terms/privacy.
- Administrator Permissions & Nonces: Secure all operations:
- Capabilities: Check
current_user_can()
for sensitive actions (e.g., only admins can save plugin settings, only editors can perform certain tasks, etc.). Do not rely solely onis_admin()
(which is not a permission check). - Nonces: Use
wp_nonce_field()
in forms and verify withcheck_admin_referer()
(for POST) orcheck_ajax_referer()
(for AJAX). Every form submission or state-changing URL should include a nonce. Verify it before processing andwp_die()
or safely fail if nonce is invalid. - Sanitize nonce values when verifying (e.g.,
sanitize_text_field( wp_unslash($_POST['myplugin_nonce']) )
).
- Capabilities: Check
- Internationalization (i18n): Make strings translatable:
- Wrap all user-facing text in
__()
,_e()
,_x()
, etc., with your text domain. This includes error messages, settings labels, front-end output, everything. - Do not concatenate translatable strings or use variables for the text or text domain. Strings must be literal for translation. E.g., use
sprintf(__('Hello %s','my-plugin'), $name)
instead of splitting “Hello ” and name. - Ensure the text domain in functions matches the one in your header (case-sensitive).
- Prepare a
.pot
file or ensure tools can scan your plugin. (The WordPress.org translation system will do this automatically if you’ve properly internationalised the plugin.) - If including your own translations, put them in
/languages
folder and load them withload_plugin_textdomain()
.
- Wrap all user-facing text in
- Performance Considerations: Avoid expensive operations on every page load:
- Don’t perform remote requests or heavy computations in the global scope of your plugin or on init without necessity. Use caching or trigger such actions on specific admin actions or via cron jobs.
- If your plugin writes to the database (options or otherwise) on every page load, reconsider (perhaps only on specific events, or use transients).
- Use appropriate hooks to initialise features only when needed (e.g., only load admin-related code in
is_admin()
context + proper capability).
- Cleanup on Uninstall: Provide an option to clean up stored data on uninstall:
- If your plugin adds options, custom tables, or custom post types, consider implementing
register_uninstall_hook
or anuninstall.php
to remove them when the user deletes the plugin. (This is recommended but not required for approval. However, if you do implement, ensure it checksdefined('WP_UNINSTALL_PLUGIN')
).
- If your plugin adds options, custom tables, or custom post types, consider implementing
- No Spam/Ads/Unauthorized Changes:
- Do not include spammy content in your readme (no keyword stuffing or competitor tags).
- Do not add hidden or unapproved output on the front-end (like a backlink or “Powered by” that the user didn’t consent to). Any front-end credit/link must be opt-in via settings.
- Do not auto-modify other plugins or core settings. For example, do not deactivate or alter other plugins’ behavior (guideline: one plugin should not change another plugin’s activation state), except informing the user or stopping your own plugin if requirements aren’t met.
- If your plugin has an upsell or notices for a pro version, keep them minimal and not constantly nagging. They must not hijack the admin experience or annoy users (e.g., a one-time notice or a dedicated settings page section is acceptable; a persistent dashboard banner is not).
- Readme.txt Requirements: Ensure your
readme.txt
follows the standard:- Stable tag is set and corresponds to a tag or “trunk”. Use a version number for stable tag (e.g.,
Stable tag: 1.0.0
). Do not leave it attrunk
for new plugins. - Requires at least, Tested up to, Requires PHP are present and updated.
- The short description (first line after plugin name) is <= 150 characters and plain text.
- Contributors: list your WordPress.org username (and others if applicable).
- Tags: up to 5 relevant tags. No brand names or irrelevant tags just to appear in searches.
- Sections: Include “Description”, “Installation”, “FAQ” (if useful), “Changelog” (highly recommended to update for each version), and possibly “Screenshots” (with matching image files named screenshot-#.png in the assets).
- If your plugin relies on a service or has special notes (like needing cURL, or being a library), mention that in FAQ or description.
- Proofread the readme; it’s the public face of your plugin on the repo.
- Stable tag is set and corresponds to a tag or “trunk”. Use a version number for stable tag (e.g.,
- SVN Usage for Release:
- Prepare your plugin for initial commit: structure repository with
trunk
(latest code) and create atags/x.y.z
for the release. - Update the plugin version in main file and readme before tagging. Version in main file = tag folder name = Stable Tag in readme for consistency.
- For subsequent updates: bump version, commit to trunk, then copy trunk to a new tag folder and update Stable Tag.
- Don’t commit unnecessary files to SVN (no .git, node_modules, etc. in trunk or tags). Keep the repo lean.
- Prepare your plugin for initial commit: structure repository with
- During Review:
- Be responsive: monitor the email used for your WordPress.org account. If the plugin team emails with issues, address them promptly.
- Only after approval, share your plugin’s url or announce it. (Before approval, the plugin is not publicly visible.)
- Enable “Enable Auto-updates” for your plugin on your test sites to catch any update issues.
- Set up Two-Factor Authentication on WP.org (required for committing new plugins and a good practice for security).
- Post-Release:
- Support: Check your plugin’s support forum (wordpress.org/support/plugin/your-plugin) regularly and be helpful.
- Maintain: Update “Tested up to” when new WP versions release; update code for compatibility if needed.
- Security: If a vulnerability is reported, fix immediately and release a new version. You can contact [email protected] you need to trigger a forced update for a critical fix.
- Follow plugin guidelines for any future changes (e.g., if you add a remote call or new feature, ensure it still complies with all of the above).
Use this checklist to review your plugin line-by-line and feature-by-feature. Checking off every item will greatly improve your plugin’s chances of a swift approval and provide users with a safe, reliable experience. Compliance is not just about approval, but also about quality – a plugin that meets these standards is likely to have fewer bugs, security issues, and user complaints. Good luck with your plugin development and launch!
Conclusion
WordPress powers roughly 43% of all sites today and with more than 59 000 free plugins already live according to bloggerspassion.com and review time reducing with weeks thanks to the Plugin Check tool, the directory is busy yet surprisingly accessible. However, being able to publish a plugin to WordPress is still no mean feat.
Using this guide to build a WordPress plugin and navigate the rigorous process of publishing your plugin to WordPress.org will significantly enhance your chances of success and reduce a lot of time and stress as well.
If you follow the coding standards, prefix everything, and give sanitisation the same love you give features; most avoidable rejections will vanish.
Avoid the lessons I paid for in sweat and invoices. Use it as a checklist, enhance your AI agents or co-pilots with useful markdown instructions, polish your readme, and keep iterating. When your plugin finally appears in the directory, put the kettle on, share the link, and listen to user feedback, because nothing beats seeing your code solve real-world headaches at scale.
If you need the help or support of an experienced web design agency, you know where to find us.
Please share your own experiences! Drop us a comment below or message us on social media if you found this guide useful or have any of your own tips to share.
0 Comments