# Plugin standards Rules every plugin made from the wpallstars starter plugin follows. This file is the same in each of them: the starter holds the master copy, so a lesson learned in one plugin goes into the starter's copy and then to every plugin (`scripts/sync-core.sh` shows the differences). Never put rules for one plugin here; they go in its `AGENTS.md`. Names below are placeholders. Each plugin's `AGENTS.md` gives its values: | Placeholder | Meaning | Example | |---|---|---| | `{slug}` | Folder, main file and text domain | `my-plugin` | | `{prefix}` | Option, hook, function and file prefix: `{PREFIX}` in lower case | `myplugin` | | `{Prefix}` | Class prefix (`@package`) | `MyPlugin` | | `{PREFIX}` | Constant prefix | `MYPLUGIN` | | `{Name}` | Plugin name | My Plugin | | `{css}` | CSS class and data attribute prefix (`{css}-card`, `data-{css}-setting`) | `mp` | Minimums: **WordPress 6.2, PHP 7.4** (`readme.txt`, plugin header). How changes are made and checked: `DEVELOPMENT.md`. Releases: `RELEASING.md`. ## Structure - Core files are listed in `scripts/core-files.txt`: the feature registry (`includes/class-{prefix}.php`), the base feature (`includes/class-{prefix}-feature.php`), the settings store (`includes/class-{prefix}-settings.php`), the admin screen and Read Me tab (`admin/`), the shared GitHub updater (`includes/github-updater/`), the scripts, the CI workflow and tool configuration, and the shared docs (this file, `DEVELOPMENT.md`, `RELEASING.md`, `CONTRIBUTING.md`, `SECURITY.md`). A plugin's copy differs from the starter's only in the names above. They hold no code for one plugin: they read `{Prefix}_Setup` (`includes/class-{prefix}-setup.php`: features, settings tabs, header links, settings version and history, the plugin's own helpers and admin parts) or use hooks (`{prefix}_admin_tabs` for tabs, `{prefix}_admin_enqueue` for the plugin's own admin CSS and JS, `scripts/preflight-plugin.sh` for its own release checks). Anything only one plugin needs goes there, in a feature, or in its own file loaded from there. To change a core file, change the starter first, then run `scripts/sync-core.sh` in each plugin; `scripts/sync-core.sh --check` lists core files that differ. A new plugin starts as a copy of the starter renamed with `scripts/rename-plugin.sh`. - One class per feature in `includes/features/`, extending `{Prefix}_Feature`, registered in `{Prefix}_Setup::FEATURES`. Features some builds leave out go in `{Prefix}_Setup::OPTIONAL_FEATURES` and load only when present. - `settings()` declares the schema; the admin screen renders, searches and saves it with no extra code. Field types and keys: `README.md` → Developers. - `boot()` returns early unless `self::enabled()`. Features are **off by default**; the plugin's `AGENTS.md` lists any the owner asked to be on. Turning another feature on by default needs the owner's say. - A feature that replaces another plugin sets `'replaces' => array(slug => name)` and imports that plugin's settings in `migrate()` with `self::import_setting()` (fills only unset keys). It never writes or deletes the other plugin's options. While that plugin is active the feature waits, and the Plugins screen suggests deactivating and deleting it (`admin/includes/class-replaced-plugins.php`) with no extra code. - Migrations run once per `{Prefix}_Setup::DB_VERSION`. After a release, a new or changed import needs a version bump and a line in its docblock. - New options, post meta, user meta, transients, cron hooks and files must be removed in `uninstall.php`. - `README.md` is also the plugin's Read Me tab (`admin/includes/class-readme-manager.php`), which renders headings, lists, tables, bold, italic, inline code, links (http(s) and `#heading` links, with GitHub-style heading IDs) and images from the plugin folder on a line of their own (``). Use only that Markdown, or extend the renderer in the same change. - Update `README.md` (feature section, hooks, changelog), `changelog.txt` (the user-facing changelog entry) and `readme.txt` in the same change. `readme.txt` must stay under 10 KB for WordPress.org: one short line per feature, every service the plugin contacts under External services, and only the newest version's changelog, in short. Details go in `README.md`. - `.distignore` lists files kept out of the release zip. Add new development-only files there (the preflight fails when a known one gets in), then check the build with Plugin Check. ## Code rules - PHP 7.4 syntax and WordPress 6.2 APIs. Guard newer core APIs with `function_exists()` or `method_exists()`. - Capability and nonce checks on every admin action and AJAX handler; escape on output; sanitise through the schema. - Prefix everything global with `{prefix}_`, `{Prefix}_` or `{PREFIX}_`. The shared GitHub updater is the one exception: its `wpallstars_` names are the same in every plugin, so that one copy can stand in for the others. - Admin copy: short, plain words, sentence case, no jargon. - Decide for the user. Within a feature, the plugin makes the choices (which plugins, screens or items it applies to) from what it can detect. Settings are there to bypass something that causes a problem, not choices people need to understand first. A new setting must earn its place; prefer detecting the right behaviour plus a short bypass list. - Site owner in control, performance first: the owner decides what their site sends, contacts and shows. Calls to outside services are opt-in where they are not the point of the feature, made only as often and for as long as needed (cache answers, never on every page load), and never block a visitor's page when they can run later. Features that rein in other plugins hand the choice to the owner instead of deciding for them. WordPress update checks and downloads are the exception: leave them alone (next rule). - Do not change WordPress update behaviour (update transients, `auto_update_*` filters, update checks) outside the shared GitHub updater. Plugin Check reports `plugin_updater_detected` as an error, and WordPress.org asks plugins not to interfere with the updater. - Leave no PHP errors, warnings, notices or deprecations behind. Fix any that the plugin causes as you find them, in the same change when it is small, or as a tracked issue. That includes ones in other plugins that only happen because of this one. Messages that other plugins cause on their own are theirs: mention them, do not hide them. ## Updates from GitHub The one exception to the update rule, at the owner's request (for speed, reliability and site owners' control, and because the plugins are released on GitHub first): the shared GitHub updater in `includes/github-updater/`. It replaces Git Updater. - Every plugin made from the starter carries a copy. Each copy registers its version from `load.php` when its plugin loads; on `plugins_loaded` only the newest copy loads, once, and serves every installed plugin with a `GitHub Plugin URI` header. One update check covers them all. - It adds GitHub releases of those plugins to core's own update check and `plugins_api`, and leaves the download, install, auto-updates and rollback to core. It only adds entries for those plugins; it never removes or blocks other updates. - It is the same in every plugin apart from its text domain and `@package`. Change it in the starter, raise the version in its `load.php`, and copy it to each plugin. Plugins change what it does only through its filters (`wpallstars_github_updater_enabled`, `wpallstars_github_updater_early`, `wpallstars_github_plugins`, `wpallstars_github_token`), never by calling its class: another plugin's copy may be the one that runs. - Keep anything that installs or updates code from outside WordPress.org in that folder (and in a feature listed in `.distignore-wporg`, when a plugin has a setting for it), because the WordPress.org build leaves them out. - Never add an `Update URI` header. Tokens for private repositories come only from `wp-config.php` (`WPALLSTARS_GITHUB_TOKEN`) or the filter, go only to api.github.com and are never stored. ## Releases GitHub releases are the early channel; WordPress.org gets settled versions. Sites install the latest GitHub release whose tag is a plain version and the asset whose name starts with the plugin folder (the shared updater; Git Updater, where still active, reads `Version:` on `main` instead), so: - Publish the GitHub release (tag `vX.Y.Z`, asset `{slug}-X.Y.Z.zip` with a `{slug}/` folder, built with `.distignore`) straight after the version change reaches `main`. - Never put a pre-release version (`-beta1`, `-rc1`) in `Version:` on `main`; mark test releases as pre-releases on GitHub. - The WordPress.org build is the release build without the files in `.distignore-wporg` (the GitHub updater) and the `GitHub Plugin URI`, `Primary Branch` and `Release Asset` header lines. Its zip is named `wordpress-org-{slug}-X.Y.Z.zip` so no updater picks it; never attach it to a GitHub release. - Any plugin released on GitHub (made from the starter or not) follows the same pattern: a `GitHub Plugin URI: owner/repo` header (and `Release Asset: true`), plain version tags, and a `{folder}-X.Y.Z.zip` asset with a `{folder}/` inside. - Build both zips with `scripts/build-release.sh`, check them with `scripts/preflight-release.sh` and `scripts/plugin-check.sh`. None of them tags, publishes or uploads anything. - Releasing and submitting to WordPress.org need the owner's say. A private repository cannot be read by sites without a token (`WPALLSTARS_GITHUB_TOKEN`). Details: `RELEASING.md`; the plugin's own submission state: `LAUNCH.md`. ## Front-end styling and dark mode Block, shortcode and other front-end styles must work with the Kadence Pro dark mode switcher (and themes that switch palettes the same way). - How it switches: Kadence adds `color-switch-dark` or `color-switch-light` to `
`. The dark class sets `color-scheme: dark` and redefines `--global-palette1`…`15` and `--wp--preset--color--theme-palette-N` **on ``**. `` stays `color-scheme: light`. Palette 3 is the strongest text and palette 9 the page background in light mode; dark mode swaps them. - Use `currentColor`, `inherit`, translucent neutrals (for example `rgba(127, 127, 127, 0.12)`) or palette variables, never fixed light or dark colours for text, backgrounds or borders. - Do not use `@media (prefers-color-scheme)` to follow the site: it tracks the visitor's system, not the switcher. - Do not define custom properties on `:root` from palette variables; they resolve above `` and keep the light values. Read palette variables where they are used, or define derived ones on the block. - Preset references (`var:preset|color|theme-palette3`) become CSS variables with kebab-cased slugs, as core does: `--wp--preset--color--theme-palette-3`. Use `_wp_to_kebab_case()` in PHP and the same rule in editor JS (a hyphen between letters and digits, lower case). - Embedded pages (iframes) do not follow the switcher: they see the visitor's system setting, and the browser paints their own background behind them, so they stay readable in both modes. Do not make iframes transparent or tint them. - Test light and dark with the Kadence theme by toggling the body class (see Testing, step 5). ## Testing CI (`.github/workflows/ci.yml`) runs the code checks, the release preflight, Plugin Check and a smoke test on every pull request; details and local commands: `DEVELOPMENT.md`. While a repository is private, CI and review apps only advise: nothing is required to merge, for speed. Fix failures your change causes before merging; open an issue for any other failure and merge anyway. Do not turn on branch protection, required checks or paid reviewers. At public launch (owner's say), run the full sweep in `DEVELOPMENT.md` → At public launch, which makes the checks required. The checks catch errors, not wrong behaviour, so also verify on real WordPress: 1. `scripts/lint.sh` (syntax, ShellCheck, PHPCS, PHPStan; run `composer install` once). Fix findings in the code; never grow `phpstan-baseline.neon` or add a `phpcs:ignore` without a reason. 2. The user reviews on one local test site per plugin, shared by every session and worktree. It shows a **combined preview**: `origin/main` plus every open pull request from the repository, merged together. Update it only with the script, from any worktree: ```bash scripts/preview-site.sh # the first run on a clone takes the site: scripts/preview-site.sh "