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

162 lines
7.9 KiB
Markdown

---
name: wordpress-plugin-rebrand
description: 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. `MetAI`
`iWP MetAI`). Do this LAST, after class renaming, or you'll double-
prefix the class name.
7. **File/directory renames** to match: `class-oldname-admin.php`
`class-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:
```php
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:
```php
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.