162 lines
7.9 KiB
Markdown
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.
|