Scaffold a new WordPress plugin in packages/plugins/.
Create a new WordPress plugin in packages/plugins/ following the project's established conventions.
Ask the user: "What will this plugin do?" — a brief description of its purpose and the problem it solves.
Based on the description, suggest 2–3 slug and name pairs, for example:
Based on your description, here are some suggestions:
# Slug Name 1 event-managerEvent Manager 2 eventsEvents 3 wp-eventsWP Events Which would you like to use, or do you have your own preference?
Once the purpose and identity are confirmed, apply the find-plugin skill to determine whether:
Only proceed to the steps below if Core and existing plugins do not cover the need, or if the user has explicitly agreed to build a custom plugin after reviewing the recommendation.
| Input | Required | Notes |
|---|---|---|
| Plugin slug | Yes | kebab-case, e.g. acme-blocks. Becomes the directory name, text domain, and Composer package name. |
| Plugin title | Yes | Human-readable name for the Plugin Name: header, e.g. Acme Blocks |
| Plugin description | Yes | Short description for the Description: plugin header |
| PHP namespace | Yes | PascalCase, e.g. Acme\Blocks. Check sibling plugins for the project's convention. |
| Plugin type | Yes | block-library (contains Gutenberg blocks) or general (functionality plugin, no blocks) |
| Project vendor prefix | Yes (block-library only) | e.g. acme — used for block names like acme/hero-banner. Check sibling plugins' block.json files to confirm. |
Scan packages/plugins/ for an existing plugin to use as a structural reference.
Block-library plugin:
packages/plugins/<plugin-slug>/
├── plugin.php (plugin header + block auto-loader)
├── src/ (block source directories — see add-block skill)
├── languages/
│ └── .gitkeep
├── package.json
├── composer.json
└── phpcs.xml
General plugin:
packages/plugins/<plugin-slug>/
├── plugin.php (plugin header + bootstrap)
├── src/
│ ├── Plugin.php (main class, singleton)
│ ├── Admin/ (admin screens — add if needed)
│ ├── Api/ (REST controllers — add if needed)
│ └── Frontend/ (front-end hooks — add if needed)
├── languages/
│ └── .gitkeep
├── package.json
├── composer.json
└── phpcs.xml
plugin.phpBlock-library variant:
<?php
/**
* Plugin Name: <Plugin Title>
* Description: <Plugin description>
* Version: 1.0.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: Dekodeinteraktiv
* Text Domain: <plugin-slug>
* Domain Path: /languages
*
* @package <PhpNamespace>
*/
declare( strict_types=1 );
namespace <PhpNamespace>;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Load all blocks (auto-discovered from build/).
foreach ( glob( __DIR__ . '/build/*/block.php' ) as $block ) {
require_once $block;
}
/**
* Load plugin text domain.
*/
function load_textdomain(): void {
load_plugin_textdomain( '<plugin-slug>', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
add_action( 'init', __NAMESPACE__ . '\\load_textdomain' );
General plugin variant:
<?php
/**
* Plugin Name: <Plugin Title>
* Description: <Plugin description>
* Version: 1.0.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: Dekodeinteraktiv
* Text Domain: <plugin-slug>
* Domain Path: /languages
*
* @package <PhpNamespace>
*/
declare( strict_types=1 );
use <PhpNamespace>\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/vendor/autoload.php';
Plugin::instance()->hooks();
src/Plugin.php (general plugin only)<?php
/**
* Main plugin class.
*
* @package <PhpNamespace>
*/
declare( strict_types=1 );
namespace <PhpNamespace>;
/**
* Plugin class.
*/
class Plugin {
private static ?self $instance = null;
private function __construct() {}
public static function instance(): self {
return self::$instance ??= new self();
}
public function hooks(): void {
add_action( 'init', [ $this, 'init' ] );
add_action( 'init', [ $this, 'load_textdomain' ] );
// Registration hooks must be called in the plugin bootstrap file,
// but we keep them here for discoverability.
register_activation_hook( __FILE__, [ $this, 'activate' ] );
register_deactivation_hook( __FILE__, [ $this, 'deactivate' ] );
}
public function init(): void {
// Register CPTs, taxonomies, etc.
}
public function load_textdomain(): void {
load_plugin_textdomain( '<plugin-slug>', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
public function activate(): void {
flush_rewrite_rules();
}
public function deactivate(): void {
flush_rewrite_rules();
}
}
composer.json{
"name": "dekode/<plugin-slug>",
"description": "<Plugin description>",
"type": "wordpress-plugin",
"version": "1.0.0",
"require": {},
"autoload": {
"psr-4": { "<PhpNamespace>\\": "src/" }
},
"scripts": {
"make-pot": "wp i18n make-pot . languages/<plugin-slug>.pot --domain=<plugin-slug> --exclude=vendor,node_modules,build",
"make-json": "wp i18n make-json languages/<plugin-slug>.pot languages --no-purge",
"lint": "@php vendor/bin/phpcs",
"lint-fix": "@php vendor/bin/phpcbf"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
package.json{
"name": "@dekode/<plugin-slug>",
"version": "1.0.0",
"private": true,
"description": "<Plugin description>",
"scripts": {
"build": "wp-scripts build --webpack-copy-php",
"start": "wp-scripts start --webpack-copy-php",
"lint:js": "wp-scripts lint-js src",
"lint:css": "wp-scripts lint-style src",
"format": "wp-scripts format src"
},
"devDependencies": {
"@wordpress/scripts": "*"
}
}
dependencies needed by blocks (e.g. @phosphor-icons/react for icons).phpcs.xml<?xml version="1.0"?>
<ruleset name="<Plugin Title>">
<description>PHP CodeSniffer ruleset for <Plugin Title>.</description>
<file>.</file>
<arg name="extensions" value="php"/>
<arg name="basepath" value="."/>
<exclude-pattern>build/</exclude-pattern>
<exclude-pattern>vendor/</exclude-pattern>
<exclude-pattern>node_modules/</exclude-pattern>
<rule ref="../../../phpcs.xml.dist"/>
</ruleset>
The ../../../phpcs.xml.dist path resolves to the root ruleset. Adjust the relative path if the plugin nesting differs.
Root package.json — add the plugin to the workspaces array:
"workspaces": [
"packages/plugins/<plugin-slug>"
]
Root composer.json — add a path repository and require the package:
"repositories": [
{
"type": "path",
"url": "packages/plugins/<plugin-slug>"
}
]
And in require:
"dekode/<plugin-slug>": "*"
composer install # symlinks plugin into public/content/plugins/
npm install # registers workspace
npm run build # verify build passes
npm run lint:js
npm run lint:css
composer lint
After composer install, confirm the plugin is symlinked into public/content/plugins/<plugin-slug>/ and appears in the WordPress plugin list.
Confirm namespace matches the PSR-4 map in composer.json.
build/*/block.php via glob — never require individual blocks manually in plugin.phppublic/: Composer symlinks the plugin automatically; never manually copy files into public/content/vendor/: All dependencies managed by ComposerWhen done:
packages/plugins/<plugin-slug>/plugin.php — WordPress plugin entry pointpackages/plugins/<plugin-slug>/composer.json — PHP package metadatapackages/plugins/<plugin-slug>/package.json — JS build configpackages/plugins/<plugin-slug>/phpcs.xml — PHP linting rulesetpackages/plugins/<plugin-slug>/src/ — source directory (blocks for block-library; PHP classes for general)packages/plugins/<plugin-slug>/languages/ — translations directoryThe plugin is symlinked into public/content/plugins/<plugin-slug>/ after composer install and is ready to receive blocks via the add-block skill.