| name | woo-marketplace-extension |
| description | Skill for developing paid plugins for the WooCommerce.com Marketplace. Covers plugin scaffolding, file structure, required headers, HPOS compatibility, Blocks/StoreAPI integration, UX guideline compliance, security, and internationalization — everything needed to pass marketplace review. Use when creating a new WooCommerce extension plugin, updating an existing plugin for marketplace compliance, or when keywords like "Woo Marketplace", "sell on WooCommerce.com", "paid plugin", or "extension development" appear. Also reference proactively when asked about WooCommerce plugin structure, HPOS compatibility, or Blocks compatibility.
|
WooCommerce Marketplace Extension Development
Plugins sold on the WooCommerce.com marketplace share the same foundation as standard WordPress
plugins, but must meet additional quality standards, UX conventions, and technical requirements.
This skill covers all requirements needed to pass marketplace review.
Plugin Structure
Scaffold
Use create-woo-extension or manually set up the following structure:
my-extension/
├── my-extension.php # Main plugin file (must match directory name)
├── includes/ # PHP classes
│ ├── class-main.php
│ ├── class-admin.php
│ └── class-frontend.php
├── src/ # JS/React source (built with @wordpress/scripts)
│ └── index.js
├── build/ # Build output (generated by npm run build — do not edit directly)
├── assets/
│ ├── css/
│ └── images/
├── languages/
│ └── my-extension.pot # Translation template
├── templates/ # Template override directory
├── changelog.txt # Required: validated during marketplace upload
├── readme.txt
├── composer.json
├── package.json
├── .wp-env.json # Development environment configuration
└── uninstall.php # Cleanup on uninstall
Keep the directory name, main filename, and text domain all identical (hyphen-separated, no underscores).
Required Plugin Header
<?php
The Author / Developer field is required. Plugin names should clearly describe the feature
(e.g., "Appointments" rather than "VendorXYZ Bookings Plugin for WooCommerce").
Initialization Pattern
defined( 'ABSPATH' ) || exit;
add_action( 'plugins_loaded', 'my_extension_init', 10 );
function my_extension_init() {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action( 'admin_notices', 'my_extension_wc_missing_notice' );
return;
}
define( 'MY_EXTENSION_VERSION', '1.0.0' );
define( 'MY_EXTENSION_PLUGIN_FILE', __FILE__ );
define( 'MY_EXTENSION_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
require_once MY_EXTENSION_PLUGIN_DIR . 'includes/class-main.php';
My_Extension\Main::instance();
}
Initialize at plugins_loaded priority 10 to ensure WooCommerce-related functionality loads first.
Activation / Deactivation / Uninstall
register_activation_hook( __FILE__, 'my_extension_activate' );
register_deactivation_hook( __FILE__, 'my_extension_deactivate' );
function my_extension_activate() {
}
function my_extension_deactivate() {
wp_clear_scheduled_hook( 'my_extension_cron_event' );
}
Perform full data removal in uninstall.php. Do not delete user data on deactivation.
HPOS (High-Performance Order Storage) Compatibility — Required
All new submissions must be HPOS-compatible. HPOS moves order data from wp_posts / wp_postmeta
to dedicated tables (wp_wc_orders, wp_wc_orders_meta, etc.).
Compatibility Declaration
add_action( 'before_woocommerce_init', function() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
Without this declaration in the main plugin file, WooCommerce will display an incompatibility warning.
Order Data Access Rules
What NOT to do:
$value = get_post_meta( $order_id, '_billing_email', true );
update_post_meta( $order_id, '_custom_field', $value );
$wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_type = 'shop_order'" );
Correct patterns:
$order = wc_get_order( $order_id );
$email = $order->get_billing_email();
$order->update_meta_data( '_custom_field', $value );
$order->save();
$orders = wc_get_orders( array(
'meta_key' => '_custom_field',
'meta_value' => 'target_value',
'limit' => 10,
) );
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
} else {
}
Custom Meta Boxes
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
add_action( 'add_meta_boxes', function() {
$screen = class_exists( CustomOrdersTableController::class )
&& wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id( 'shop-order' )
: 'shop_order';
add_meta_box(
'my-extension-meta-box',
__( 'My Extension', 'my-extension' ),
'render_meta_box',
$screen,
'side',
'high'
);
});
Blocks / Store API Integration
Block-based Cart/Checkout is the WooCommerce default. Extensions must integrate correctly with it.
Cart/Checkout Block Compatibility Declaration
add_action( 'before_woocommerce_init', function() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'cart_checkout_blocks',
__FILE__,
true
);
}
});
IntegrationInterface Implementation
When injecting frontend scripts or data into Cart/Checkout blocks:
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
class My_Extension_Blocks_Integration implements IntegrationInterface {
public function get_name() {
return 'my-extension';
}
public function initialize() {
$this->register_block_frontend_scripts();
$this->register_block_editor_scripts();
}
public function get_script_handles() {
return array( 'my-extension-blocks-frontend' );
}
public function get_editor_script_handles() {
return array( 'my-extension-blocks-editor' );
}
public function get_script_data() {
return array(
'option_value' => get_option( 'my_extension_setting', '' ),
);
}
}
add_action( 'woocommerce_blocks_loaded', function() {
add_action(
'woocommerce_blocks_checkout_block_registration',
function( $integration_registry ) {
$integration_registry->register( new My_Extension_Blocks_Integration() );
}
);
});
Custom Data via Store API
use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema;
woocommerce_store_api_register_endpoint_data( array(
'endpoint' => CartSchema::IDENTIFIER,
'namespace' => 'my-extension',
'data_callback' => function() {
return array( 'custom_data' => 'value' );
},
'schema_callback' => function() {
return array(
'custom_data' => array(
'description' => 'Custom data from My Extension',
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
},
));
woocommerce_store_api_register_update_callback( array(
'namespace' => 'my-extension',
'callback' => function( $data ) {
WC()->session->set( 'my_extension_data', sanitize_text_field( $data['value'] ) );
},
));
UX Guidelines — Reviewed Carefully During Audit
Menu Placement
Creating top-level menu items is not allowed. Integrate into the existing WooCommerce menu structure:
add_action( 'admin_menu', function() {
add_submenu_page(
'woocommerce',
__( 'My Extension Settings', 'my-extension' ),
__( 'My Extension', 'my-extension' ),
'manage_woocommerce',
'my-extension',
'render_settings_page'
);
});
add_filter( 'woocommerce_settings_tabs_array', function( $tabs ) {
$tabs['my_extension'] = __( 'My Extension', 'my-extension' );
return $tabs;
}, 50 );
What NOT to Do
- Insert proprietary telemetry or tracking code
- Display large banners, branding, or advertisements in WP Admin
- Include upsell links, affiliate links, or spam links to sites outside the marketplace
- Request reviews on first launch (show only after setup completion or extended use)
- Alter the appearance of core interfaces (e.g., restyling containers)
Best Practices
- Reuse existing WordPress / WooCommerce UI components
- Keep guidance text within interfaces to 120–140 characters; move details to documentation
- Ensure mobile responsiveness (merchants check their stores on mobile 24/7)
- Keep the setup flow simple and guide users toward success
Security
Automatically scanned by the QIT Security Test. Enforce the following:
if ( ! wp_verify_nonce( $_POST['_nonce'], 'my_extension_action' ) ) {
wp_die( 'Security check failed' );
}
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( 'Unauthorized' );
}
$value = sanitize_text_field( wp_unslash( $_POST['field'] ?? '' ) );
$email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
$int = absint( $_POST['count'] ?? 0 );
echo esc_html( $value );
echo esc_attr( $attribute );
echo esc_url( $url );
echo wp_kses_post( $html_content );
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_table WHERE id = %d AND status = %s",
$id,
$status
)
);
Do not store sensitive data (API keys, etc.) in plain text in wp_options. Encrypt them, or
use an OAuth flow for external service authentication.
Minimize use of third-party libraries. When used, include only the required portions and
take full responsibility for their security. Prefer WordPress-compatible libraries where available.
Internationalization (i18n)
Write all text in English and wrap it in translation functions:
__( 'Settings saved.', 'my-extension' )
_e( 'Enable feature', 'my-extension' )
esc_html__( 'Order total', 'my-extension' )
sprintf(
__( 'Order #%s has been updated.', 'my-extension' ),
$order_number
)
sprintf(
_n( '%d item', '%d items', $count, 'my-extension' ),
$count
)
The text domain must match the plugin directory name.
For the Japanese market, bundle languages/my-extension-ja.po / .mo.
Generate the POT file with: wp i18n make-pot . languages/my-extension.pot
For JavaScript translations, use wp_set_script_translations():
wp_set_script_translations( 'my-extension-script', 'my-extension', plugin_dir_path( __FILE__ ) . 'languages' );
Extensibility
Expose appropriate actions and filters so other developers can customize behavior:
do_action( 'my_extension_before_process', $order );
do_action( 'my_extension_after_process', $order, $result );
$settings = apply_filters( 'my_extension_default_settings', array(
'enabled' => 'yes',
'title' => __( 'Default Title', 'my-extension' ),
));
function my_extension_get_template( $template_name, $args = array() ) {
$template = locate_template( 'my-extension/' . $template_name );
if ( ! $template ) {
$template = MY_EXTENSION_PLUGIN_DIR . 'templates/' . $template_name;
}
if ( $args ) {
extract( $args, EXTR_SKIP );
}
include $template;
}
Coding Standards
- Follow WordPress Coding Standards (PHPCS with the
WordPress-Extra ruleset)
- Add PHP DocBlock comments to all functions
- Avoid God Objects; follow the single responsibility principle
- Separate business logic from presentation logic
- Confirm zero errors and warnings with
WP_DEBUG enabled
- Do not use classes under the
Automattic\WooCommerce\Internal namespace (no backward compatibility guarantee)
- Do not use code marked
@internal
changelog.txt
The existence and format of changelog.txt is validated during marketplace upload:
*** My Extension Changelog ***
= 1.0.0 - 2025-04-01 =
* Feature - Initial release
* Feature - HPOS support
* Feature - Block checkout compatibility
* Tweak - Improved settings UI
* Fix - Fixed validation error on checkout
The version number must match the Version field in the plugin header.