WP Manifestindependent plugin directory
manifest / developer / wordpress-plugin-starter

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

0stars
0forks

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.zip

Boilerplate 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.scssadmin/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:

  1. Increments the version in package.json, composer.json and the main PHP file
  2. Compiles SCSS to minified CSS
  3. 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:fix manually 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