Files
agent-skills/skills/wordpress-plugin-rebrand/SKILL.md
T

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_Updater class), 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

  1. 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.
  2. Constants: OLDNAME_NEWPREFIX_OLDNAME_.
  3. Classes: OldName_NewPrefix_OldName_.
  4. Functions / hooks / underscore contexts: oldname_newprefix_oldname_.
  5. camelCase-after-underscore contexts (JS localize-object names are the common case: oldnameData used as the 2nd arg to wp_localize_script()) → newprefix_oldnameData. Grep the JS files for the exact existing spelling before finalizing — don't guess the casing, the PHP wp_localize_script() call must match byte-for-byte or the JS breaks silently at runtime.
  6. Remaining bare prose occurrences of the product name (page titles, UI labels, doc-comments) → the new display name (e.g. MetAIiWP MetAI). Do this LAST, after class renaming, or you'll double- prefix the class name.
  7. File/directory renames to match: class-oldname-admin.phpclass-newprefix-oldname-admin.php, etc.
  8. 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_loaded itself — 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_action call runs) BEFORE plugins_loaded ever 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 a public static function instance() pattern), do NOT add a nested add_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) — never plugins_loaded itself — 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

  1. php -l every .php file. Zero tolerance for syntax errors.
  2. 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.
  3. Sanity-check for double-prefixing bugs (iWP iWP, IWP_IWP, iwp-iwp, iwp_iwp or 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 -l yourself, a fresh grep, an actual functional test through the real serving pipeline) is what actually confirms it, not the report.