My Plugin
A modern WordPress plugin boilerplate with tabbed settings UI
by ThachPN165 · github.com/thachpn165/wp-starter-plugin · 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/thachpn165/wp-starter-plugin/archive/refs/heads/main.zipWordPress Plugin Starter
A production-ready WordPress plugin boilerplate that provides a complete development environment out of the box. Built with modern standards (PSR-4 autoloading, WordPress Coding Standards, Vite for assets), this boilerplate helps you skip the setup and focus on building features. Easily extensible architecture with clean separation of concerns.
Requirements
- WordPress 6.0+
- PHP 7.4+
- Composer
- Node.js 18+
- Docker (optional)
Quick Start
# Clone the repository
git clone https://github.com/thachpn165/wp-starter-plugin.git your-plugin-name
cd your-plugin-name
# Initialize your plugin
./scripts/init.sh
The init script will:
- Prompt for plugin name, slug, and namespace
- Replace all boilerplate strings
- Generate unique Docker ports (random, avoids conflicts)
- Create
.envfile with configuration - Install Composer and NPM dependencies
- Build assets
- Optionally initialize a new git repository
After initialization:
# Start Docker
docker-compose up -d
# Check your unique ports in terminal output or .env file
Development
Docker Environment
Docker mounts dist/my-plugin/ (not source) to prevent WordPress from overwriting your files.
Development workflow:
# Terminal 1: Build + watch for changes
npm run dev:docker
# Terminal 2: Start Docker
docker-compose up -d
Or build once and start:
./scripts/build.sh build
docker-compose up -d
Docker commands:
docker-compose up -d # Start
docker-compose down # Stop
docker-compose logs -f # View logs
Ports are defined in .env (generated by init.sh).
Build Commands
./scripts/build.sh build # Build to dist/
./scripts/build.sh dev # Build + watch for changes
./scripts/build.sh zip # Create ZIP archive
./scripts/build.sh deploy-svn # Deploy to SVN structure
./scripts/build.sh version X.X # Bump version
./scripts/build.sh clean # Clean outputs
Code Quality
composer phpcs # Run PHPCS
composer phpcbf # Auto-fix errors
composer test # Run tests
Assets
npm run dev # Watch Vite assets only
npm run dev:docker # Build + sync watch for Docker dev
npm run dev:sync # Sync watch only (no initial build)
npm run build # Production build
Directory Structure
your-plugin/
├── src/
│ ├── Admin/
│ │ ├── AdminMenu.php # Menu registration, AJAX handler
│ │ ├── SettingsPage.php # Main settings page wrapper
│ │ └── Tabs/ # Modular tab components
│ │ ├── DashboardTab.php
│ │ ├── GeneralTab.php
│ │ ├── AdvancedTab.php
│ │ └── IntegrationsTab.php
│ ├── Core/ # Core classes
│ ├── PublicSide/ # Public functionality
│ ├── Interfaces/
│ └── Traits/
├── assets/
│ ├── src/ # Source (scss, js)
│ ├── css/ # Compiled CSS
│ └── js/ # Compiled JS
├── scripts/ # Build scripts
├── docker/ # Docker configs
├── dist/ # Build output (Docker mounts this)
└── languages/ # Translations
Features
- Tabbed Settings UI: Dashboard, General, Advanced, Integrations tabs
- Modular Tab Architecture: Each tab is a separate class for easy customization
- AJAX Save: No page reload, toast notifications
- Dashboard Tab: Plugin info, usage guides with accordion
- Build Scripts: ZIP, SVN deploy, version bump
- Docker Ready: Random ports, no conflicts with other projects
Examples
Adding a New Setting Field
In src/Admin/Tabs/GeneralTab.php, add to render() method inside the <table>:
<tr>
<th scope="row">
<label for="my_new_option"><?php esc_html_e( 'My New Option', 'your-plugin' ); ?></label>
</th>
<td>
<input type="text" id="my_new_option" name="my_new_option"
value="<?php echo esc_attr( $settings['my_new_option'] ?? '' ); ?>"
class="regular-text" />
</td>
</tr>
In src/Admin/AdminMenu.php, update ajax_save_settings() and sanitize_settings():
// In ajax_save_settings()
'my_new_option' => sanitize_text_field( wp_unslash( $_POST['my_new_option'] ?? '' ) ),
// In sanitize_settings()
$sanitized['my_new_option'] = sanitize_text_field( $input['my_new_option'] ?? '' );
// In get_default_settings()
'my_new_option' => '',
Adding a New Tab
- Create
src/Admin/Tabs/CustomTab.php:
<?php
namespace ThachPN165\YourPlugin\Admin\Tabs;
defined( 'ABSPATH' ) || exit;
class CustomTab {
public static function render( array $settings ): void {
?>
<div class="my-plugin-tab-content" id="tab-custom">
<h2><?php esc_html_e( 'Custom Settings', 'your-plugin' ); ?></h2>
<table class="form-table">
<!-- Your fields here -->
</table>
</div>
<?php
}
}
- Register in
src/Admin/SettingsPage.php:
// Add import at top
use ThachPN165\YourPlugin\Admin\Tabs\CustomTab;
// Add to get_tabs()
'custom' => array(
'label' => __( 'Custom', 'your-plugin' ),
'icon' => 'dashicons-star-filled',
),
// Add render call in render_content()
<?php CustomTab::render( $settings ); ?>
Adding Custom Admin Page
Create src/Admin/CustomPage.php:
<?php
namespace ThachPN165\YourPlugin\Admin;
class CustomPage {
public static function render(): void {
echo '<div class="wrap"><h1>Custom Page</h1></div>';
}
}
Register in src/Admin/AdminMenu.php:
add_submenu_page(
'your-plugin',
__( 'Custom Page', 'your-plugin' ),
__( 'Custom', 'your-plugin' ),
'manage_options',
'your-plugin-custom',
array( CustomPage::class, 'render' )
);
License
GPL-2.0+