Skip to content
Ultivo Toolkit 1.4 is here! Get 10% off your first year with LAUNCH10 until October 30. See pricing

Articles

What the wordpress.org plugin review actually asks for

We submitted our first plugin to the wordpress.org directory at the end of August 2026. It was approved on 8 September, after five rounds. Almost none of the delay was code quality.

The handbook covers most of what they check. What it does not prepare you for is which rules are absolute, which replies are final, and which of your own habits will quietly break things while you fix everything else. This is the list we wish we had read first.

The name is the most expensive thing to get wrong#

Our first submission was rejected automatically, within a second. A display name or permalink cannot begin with "wp". There is no appeal and no nuance: it is a string check.

So we renamed and submitted again. That version cleared the automated scan, and then a human turned it down. "Toolkit" is a generic description, they said, and "Ultimate" is a promotional modifier rather than a distinctive identifier.

Two sentences in that reply are worth more than the rest of this article:

The absence of an exact match in the directory does not make the name distinctive.

Checking that your name is free is not the test. Distinctiveness is the test, and it is judged on the words themselves.

Other published plugins do not set a precedent for this submission; some were accepted under earlier standards.

We had a list of near-identical names approved as recently as the year before. It does not count. Pointing at them does not help, and the reply says so before you can ask.

What passes is a coined word or a personal or brand identifier, and it has to be at the front of the name, not tacked on at the back. We ended up inventing one.

Two practical notes. The slug is permanent and the display name is not, so keep the slug short and brandable and put the descriptive part in the name. And if your Author URI or Plugin URI points at a domain that is not obviously connected to the email you registered with, they will ask you to prove you own it. A TXT record on the root is enough. Add it next to your SPF record rather than replacing it: a root domain can hold several TXT records, and overwriting the SPF one breaks your mail.

Your prefix has to be more than four characters#

Ours was three. The requirement is stated plainly, and it applies to everything: namespaces, constants, option names, post type slugs, hooks, script handles and every public template function.

Do this before you submit. If your plugin is already published, changing the prefix is a breaking change for every site that uses it, and for every theme that calls your functions.

While renaming, we hit something that applies whether or not you ever submit anything:

WordPress builds a submenu page's screen ID from the menu title, not the slug. It is sanitize_title( $menu_title ) . '_page_' . $submenu_slug. We had that prefix hardcoded in 31 comparisons, the kind you write in admin_enqueue_scripts to decide whether this is your screen.

After the rename, not one of them matched. Every admin screen of the plugin lost its CSS and its JavaScript. No error, no warning, php -l clean. It simply looked like the styling had evaporated.

You can no longer ship an editor for arbitrary CSS, JavaScript or PHP#

We had a code snippets feature. The reply was blunt: plugins that let users save arbitrary custom CSS, JavaScript or PHP are not accepted, please remove it.

We asked whether a CSS-only version would be acceptable, since that is narrower than what the Customizer already offers to every site. The answer was no, and they named the Customizer as exactly the reason CSS is covered too.

If you have that feature, plan for it not existing in the version you publish there.

Escaping happens at the point of output, and nothing goes inline#

Fifty-two notices, with two patterns behind them.

Escape where you echo, not where you build. A helper that escapes attributes earlier in the chain does not count, even when it is provably safe. We annotated over a hundred of those in one directory. If you do the same, render your output before and after and compare it byte for byte; that is the only cheap way to be sure you did not break something while touching a hundred files.

Every <style> and <script> goes through the enqueue chain. We argued for one exception, an engine that printed CSS on wp_head at priority 99 so that user CSS would land after the theme. We were wrong: enqueuing at priority 999 keeps exactly the same order.

Two exceptions we explained in our reply and that survived: a block render callback returning HTML the theme developer wrote themselves, and a standalone page that builds a whole document and exits, the way core's wp_die() does.

Three WordPress details that fail without an error#

None of these came from the reviewers. Two we broke ourselves while fixing the above, and one is the trap that everybody seems to hit.

sanitize_text_field() strips percent-encoded characters#

/caf%C3%A9/ comes back as /caf/. Harmless on REMOTE_ADDR, fatal on a URL path: a redirect or a route with a non-ASCII slug silently stops matching, and nothing errors.

Use esc_url_raw() for anything that is a URL. We introduced this bug by following a review instruction literally instead of choosing the function that fits the value. Sanitize for what the value is, not for what the checklist says.

Core owns the id {metabox-id}-title#

WordPress gives every metabox heading that id. Our box was ultivo-seo, and our own title input was ultivo-seo-title. Two elements shared an id, so getElementById returned the <h2>.

The character counter sat at 0/60 with text in the field, and the live preview never followed that input. It went unnoticed for about two months, through two browser passes where the screen was signed off, because the field right below it had no collision and worked perfectly.

flush_rewrite_rules() in the activation hook#

This one came from a reader, and it is the most common version of the problem. init has already run by the time your activation callback fires, so your post types are not registered yet. The flush writes the rules without them, and every custom post type URL 404s until somebody resaves permalinks.

It is worse than a first-run problem. If anything flushes while your plugin is inactive, someone saving permalinks, another plugin flushing, your rules are gone and reactivating does not bring them back either, for the same reason. We measured 23 rules, then 0, then still 0 after reactivating.

Two ways out. Call your register function first, inside the activation callback. Or set a flag at activation and flush on the next init. We took the flag, because our registration reads options and post meta to build the types and we did not want it running outside the normal init order.

One wrinkle if you take the flag route: when the flush is done, do not delete_option() the flag. Set it to 0 and leave it autoloaded. A missing option is by definition not in the autoload set, so get_option() fires its own query for it. On a site with a persistent object cache that mostly disappears, because the miss is remembered; on the many sites without one, it is one SELECT on every request, front end included, for something that happens twice a year. It showed up in our profiling as a permanent cost.

What we would do differently#

Settle the name before writing a line of code, and settle the prefix with it. Those two cost us whole rounds, and both are decided by rules you can read in an afternoon.

The rest was fair. The reviewers were right on every point except the one where we pushed back and lost, and the plugin is better for all of it.