My Plugin
Modern WordPress plugin boilerplate — PSR-4 autoloading, Twig templates, PHPCS, ESLint, Prettier, PHPUnit, Husky and automated release workflow.
by Author Name · github.com/olifil/wordpress-plugin-starter · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/olifil/wordpress-plugin-starter/archive/refs/heads/main.zipBoilerplate for developing and distributing WordPress plugins.
Requirements
| Tool | Minimum version | Purpose |
|---|---|---|
| PHP | 8.0 | Plugin runtime and tooling (PHPCS, PHPUnit) |
| Composer | 2.0 | PHP dependency management |
| Node.js | 18.0 | SCSS compilation, linting, release script |
| WP-CLI | — | i18n .pot file generation (optional) |
Getting started
1. Rename the plugin
Search and replace the following placeholders across all files:
| Placeholder | Example replacement | Used in |
|---|---|---|
my-plugin |
my-contact-manager |
File names, text domain, CSS slug |
my_plugin |
my_contact_manager |
PHP constants, function prefixes |
MY_PLUGIN |
MY_CONTACT_MANAGER |
PHP constants |
My Plugin |
My Contact Manager |
Plugin header, display name |
vendor |
yourname |
package.json, composer.json |
Vendor\MyPlugin |
YourName\YourPlugin |
PHP namespace (composer.json, class files) |
Then rename my-plugin.php to match your plugin slug.
Adapting the namespace
The PHP namespace follows the PSR-4 convention: Vendor\MyPlugin maps to the includes/ directory. After replacing the placeholders, update the autoload and autoload-dev entries in composer.json accordingly:
"autoload": {
"psr-4": {
"YourName\\YourPlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"YourName\\YourPlugin\\Tests\\": "tests/"
}
}
Then regenerate the autoloader:
composer dump-autoload
2. Update package.json
Set the "plugin" field to match your main PHP file name — the release script uses it:
{
"name": "vendor/my-plugin",
"plugin": "my-plugin.php",
"version": "1.0.0"
}
3. Install dependencies
composer install # PHP dependencies + PHPCS standards
npm install # Node dependencies + Husky pre-commit hooks
4. Start WordPress
Use any local WordPress environment you are comfortable with: WAMP, EasyPHP, XAMPP, Laragon, Local, wp-env, etc.
Once WordPress is running, copy or symlink the plugin folder into wp-content/plugins/ and activate it from the WordPress admin.
npm scripts
| Command | Description |
|---|---|
npm run sass:compile |
Compiles admin/scss/main.scss → admin/css/admin.css (production mode) |
npm run sass:watch |
Watches and recompiles on every SCSS change |
npm run lint:all |
Run all linters in sequence (ESLint + Prettier + PHPCS) |
npm run lint:js |
ESLint on all JS files |
npm run lint:js:fix |
ESLint with auto-fix |
npm run format |
Prettier — formats JS, SCSS, JSON, MD |
npm run format:check |
Prettier — check only (used in CI) |
npm run release |
Patch version bump, compilation and distribution zip creation |
npm run release:minor |
Same with a minor bump |
npm run release:major |
Same with a major bump |
Release process
npm run release runs in order:
- Increments the version in
package.json,composer.jsonand the main PHP file - Compiles SCSS to minified CSS
- Generates
release/{plugin-slug}.{version}.zip
The zip contains only production files (PHP, CSS, vendor/, languages/, assets/). It is ready to install via the WordPress interface.
Composer scripts
| Command | Description |
|---|---|
composer lint |
PHPCS — WordPress Coding Standards |
composer lint:fix |
PHPCBF — auto-fix fixable violations |
composer test |
PHPUnit — runs tests in tests/unit/ |
Pre-commit hooks
Husky runs lint-staged automatically before each commit:
- JS files — ESLint (auto-fix) + Prettier
- SCSS, JSON, YAML, MD — Prettier
- PHP files — PHPCS (check only — run
composer lint:fixmanually to fix before committing)
Continuous integration
GitHub Actions (.github/workflows/ci.yml) runs on every push and pull request to main:
- Lint JavaScript (ESLint + Prettier check)
- Lint PHP (PHPCS)
- PHP tests (PHPUnit)
The same workflow is compatible with Forgejo Actions (Codeberg).
Project structure
my-plugin/
├── my-plugin.php ← Main plugin file (header + bootstrap)
├── includes/ ← PHP classes
├── admin/
│ ├── scss/ ← SCSS source (main.scss + partials)
│ │ └── main.scss
│ ├── css/
│ │ └── admin.css ← Compiled output (do not edit manually)
│ └── views/ ← PHP admin templates
├── assets/ ← Static assets (images, icons…)
├── languages/ ← i18n files (.pot, .po, .mo)
├── tests/
│ ├── bootstrap.php ← PHPUnit bootstrap
│ └── unit/ ← Unit tests
├── scripts/
│ └── release.js ← Release automation script
├── .github/workflows/ ← CI/CD (GitHub Actions / Forgejo)
├── .husky/ ← Git hooks
├── release/ ← Distribution zips (gitignored)
├── vendor/ ← Composer dependencies (gitignored)
├── node_modules/ ← Node dependencies (gitignored)
├── package.json
├── composer.json
├── phpcs.xml ← PHPCS configuration
├── phpunit.xml.dist ← PHPUnit configuration
├── eslint.config.js ← ESLint configuration
├── .prettierrc ← Prettier configuration
├── .editorconfig ← Editor configuration
└── .wp-env.json ← WordPress local environment
Templating (Twig)
Admin views use Twig instead of plain PHP templates. Templates are .twig files located in admin/views/.
Twig documentation: twig.symfony.com/doc
Rendering a view
// Inside any method of My_Plugin (or any class that holds a My_Plugin_Template instance):
$this->template->display( 'example', [
'title' => __( 'My Page', 'my-plugin' ),
'notice' => 'Settings saved.',
'my_field' => get_option( 'my_plugin_field', '' ),
] );
WordPress helpers available in templates
| Type | Name | Description | Usage in Twig |
|---|---|---|---|
| Function | __() |
Returns a translated string | {{ __('text', 'my-plugin') }} |
| Function | _e() |
Echoes a translated string | {{ _e('text', 'my-plugin') }} |
| Function | admin_url() |
Returns a URL to the admin area | {{ admin_url('admin-post.php') }} |
| Function | wp_nonce_field() |
Outputs a hidden nonce input field | {{ wp_nonce_field('action') }} |
| Filter | esc_html |
Escapes a string for safe HTML output | {{ value \| esc_html }} |
| Filter | esc_attr |
Escapes a string for use in an HTML attribute | {{ value \| esc_attr }} |
| Filter | esc_url |
Escapes and validates a URL | {{ value \| esc_url }} |
See includes/class-template.php to register additional helpers.
Templates are cached in wp-content/cache/my-plugin/ in production and recompiled on every request when WP_DEBUG is true.
Attribution
This starter was created by Olivier Fillol (contact@olivierfillol.fr).
If you use it as a base for your own plugin, a mention in your project's README or a link back to the original repository would be appreciated — though not legally required.
Internationalisation
Use __() / _e() with your text domain throughout. To generate the .pot file:
wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin