7.9 KiB
name, description
| name | description |
|---|---|
| wordpress-plugin-rebrand | Use when forking and rebranding an existing WordPress plugin under a new name/prefix (e.g. for a white-label or iWP-branded resale product). Covers identifier renaming, repo flattening, updater bundling, and license-field wiring. |
WordPress Plugin Rebrand
Process for taking an existing WordPress plugin and rebranding it under a new name/prefix, without breaking anything or missing a spot. Validated live across 5 real plugins in one session (WooList, WooBC, WooSol, SlickWoo, MetAI → iWP-branded forks), each independently re-verified end-to-end afterward with zero defects found.
Inputs you need before starting
- Source repo (clone URL + credentials).
- New brand prefix (e.g.
iWP), new product slug (e.g.iwp-woolist) — get the exact slug from wherever the catalog/product list already defines it, never invent your own. - The updater/license-client file to bundle, if the target platform has
one (e.g. a shared
IWP_Updaterclass), and its exact integration snippet (api_url, plugin_slug, license_option names).
Step 1 — Learn the actual existing convention, don't assume
Read the plugin's main file first. Every plugin has its own prefix scheme — don't assume it matches a previous rebrand. Identify:
- Constant prefix (
PLUGINNAME_...) - Class prefix (
PluginName_...) - Function prefix (
pluginname_...) - Text domain string (usually lowercase-hyphenated, e.g.
pluginname) - Whether the plugin lives at the repo root or in a subdirectory
Step 2 — Flatten to repo root
If the plugin's files live in a subdirectory (not the repo root), move
everything up to root. Reason: many git hosts' repo-archive endpoint
(/archive/{tag}.zip-style) names the zip's top-level folder after the
repo, so a nested subdirectory inside the repo produces a
double-nested folder in the resulting distributable zip
(reponame/plugin-subdir/... instead of reponame/...). Flattening
avoids this categorically rather than requiring special-case handling
downstream.
Step 3 — Rename, in this order
- Text-domain / hyphenated contexts first: every
oldname-(CSS classes, script/style handles, asset file paths, admin page slugs) →newprefix-oldname-. A safe global find/replace, catches the vast majority of hyphenated identifiers correctly in one pass. - Constants:
OLDNAME_→NEWPREFIX_OLDNAME_. - Classes:
OldName_→NewPrefix_OldName_. - Functions / hooks / underscore contexts:
oldname_→newprefix_oldname_. - camelCase-after-underscore contexts (JS localize-object names are
the common case:
oldnameDataused as the 2nd arg towp_localize_script()) →newprefix_oldnameData. Grep the JS files for the exact existing spelling before finalizing — don't guess the casing, the PHPwp_localize_script()call must match byte-for-byte or the JS breaks silently at runtime. - Remaining bare prose occurrences of the product name (page titles,
UI labels, doc-comments) → the new display name (e.g.
MetAI→iWP MetAI). Do this LAST, after class renaming, or you'll double- prefix the class name. - File/directory renames to match:
class-oldname-admin.php→class-newprefix-oldname-admin.php, etc. - Update the plugin header block:
Plugin Name,Author,Author URI,Plugin URI,Text Domain(always hyphenated, never underscored — WordPress text domains are always hyphen-form even when everything else in that context uses underscores).
Step 4 — Cross-check enqueue calls against actual filenames
Every wp_enqueue_script(), wp_enqueue_style(), wp_localize_script()
handle/path argument must match the ACTUAL renamed filenames on disk
exactly, or the admin UI silently fails to load its JS/CSS with no error
message anywhere obvious. Check this explicitly, don't assume the
renames were consistent.
Step 5 — Bundle the updater / license client (if applicable)
Copy the shared updater class unchanged into the plugin's includes/
directory. Where you wire it in depends on the plugin's own
architecture — get this wrong and license/update-checking silently never
runs, with no error anywhere (see the full incident writeup in
wordpress-plugin-conventions's "Never add_action at a lower priority
from inside a callback on the same hook" section):
-
If the plugin is flat/procedural (no singleton class hooked to
plugins_loadeditself — the 5 original iWP forks are this shape), a top-level call in the main file is safe:require_once <PATH_CONSTANT> . 'includes/class-<updater-file>.php'; add_action( 'plugins_loaded', function () { new <UpdaterClass>( [ /* ... */ ] ); }, 5 );This works because the file itself loads (and this
add_actioncall runs) BEFOREplugins_loadedever fires — it's not nested inside another callback already running on that same hook. -
If the plugin uses a singleton class already hooked to
add_action('plugins_loaded', ['ClassName', 'instance'])(the iWP Cache / iWP Elementor Addons / iWP Image Optimize shape, and likely InformatiQ Toolkit's shape too — check for apublic static function instance()pattern), do NOT add a nestedadd_action('plugins_loaded', ..., 5)inside that class's constructor — confirmed dead code in exactly this shape, three times, in one session. Instead call it directly, immediately, inside the constructor:private function __construct() { new IWP_Updater( [ /* ... */ ] ); // direct call, no nested add_action // ...rest of the constructor's hook registrations... }IWP_Updater's own constructor only registers filters on OTHER hooks (pre_set_site_transient_update_plugins,plugins_api,upgrader_process_complete) — neverplugins_loadeditself — so there is no ordering reason to defer construction at all.
Then add ONE settings field for the license key (option name matching
license_option above) into wherever the plugin already has an admin
settings UI — follow that file's existing code style/pattern for how
other fields are registered/rendered, don't introduce a different
pattern. If the plugin has no settings UI at all, add a minimal one
rather than skipping this step — license-gated updates need somewhere
for the customer to enter the key.
Step 6 — Verify before pushing
php -levery.phpfile. Zero tolerance for syntax errors.- Case-insensitive grep the whole tree for the bare old name:
grep -rniE '(^|[^a-z_-])oldname' --include='*.php' --include='*.js' --include='*.css' .— every hit must be part of a correctly-prefixed token (newprefix-oldname,newprefix_oldname,NewPrefix_OldName, or the new display name). Zero unprefixed exceptions, except possibly an original-authorship credit comment if one genuinely exists. - Sanity-check for double-prefixing bugs (
iWP iWP,IWP_IWP,iwp-iwp,iwp_iwpor equivalent for your prefix) — a real mistake that happens when step 3.6 runs before 3.3, or runs twice.
Step 7 — Ship
Commit, push (force-push is fine for a freshly-created fork with no
other history depending on it — confirm that's actually the case before
force-pushing anything else). Tag a release. See the
gitea-release-workflow skill for the release/verification steps if the
git host is Gitea.
Common failure modes (all hit at least once during validation)
- Forgetting the flatten-to-root step → double-nested zip.
- JS localize-object name mismatch (hyphen where an underscore is required for a valid JS identifier) → admin UI JS silently breaks with no visible error.
- Trusting the delegate's self-report instead of re-verifying — every
single delegate run in the validating session initially reported
success; independent re-verification (re-running
php -lyourself, a fresh grep, an actual functional test through the real serving pipeline) is what actually confirms it, not the report.