From atum-cms-ecom
WordPress 6.x pattern library — block themes with theme.json and Full Site Editing, classic theme template hierarchy, custom plugin structure, Gutenberg custom blocks (block.json + JSX) and dynamic blocks (PHP render_callback), ACF field groups and ACF Blocks, WP-CLI custom commands, REST API endpoint registration with nonce + capability + kses sanitization, custom post types with show_in_rest, multisite switching, cron scheduling via wp_schedule_event, dbDelta schema migrations, and the full WordPress security checklist (nonces, capabilities, sanitization, escaping, i18n). Use whenever you write or review WordPress code — themes, plugins, blocks, REST endpoints, CLI scripts. Differentiates from atum-stack-backend generic PHP patterns by covering the WordPress-specific API surface that no generic PHP skill knows.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-cms-ecomThis skill uses the workspace's default tool permissions.
Ce skill agrège les patterns WordPress essentiels que Claude Code doit respecter à chaque contribution sur un projet WordPress. Il couvre **thème**, **plugin**, **bloc Gutenberg**, **REST API**, **WP-CLI** et **sécurité**.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Ce skill agrège les patterns WordPress essentiels que Claude Code doit respecter à chaque contribution sur un projet WordPress. Il couvre thème, plugin, bloc Gutenberg, REST API, WP-CLI et sécurité.
Avant d'écrire du code WordPress, vérifier qu'aucun des anti-patterns listés plus bas n'est présent. Si le code existant utilise déjà un anti-pattern (ex. SQL direct, pas de nonce), le corriger dans la même PR ou flagger comme dette technique.
Structure minimale d'un block theme moderne :
my-theme/
├── style.css # Header obligatoire (nom, version, auteur)
├── theme.json # Configuration globale : couleurs, typo, spacing
├── functions.php # Enqueue, supports, hooks
├── templates/
│ ├── index.html # Template par défaut
│ ├── single.html # Post singulier
│ ├── page.html # Page singulière
│ ├── archive.html # Archive générique
│ ├── 404.html # Erreur 404
│ └── search.html # Résultats de recherche
├── parts/
│ ├── header.html # Réutilisable
│ └── footer.html
├── patterns/
│ └── hero.php # Patterns enregistrés automatiquement si header correct
└── assets/
├── css/
└── js/
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"color": {
"palette": [
{ "slug": "primary", "color": "#0a0a0a", "name": "Primary" },
{ "slug": "accent", "color": "#ff5f1f", "name": "Accent" },
{ "slug": "base", "color": "#ffffff", "name": "Base" }
],
"custom": false,
"defaultPalette": false
},
"typography": {
"fontFamilies": [
{
"slug": "sans",
"name": "Sans",
"fontFamily": "Inter, system-ui, sans-serif"
}
],
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "base", "size": "1rem", "name": "Base" },
{ "slug": "lg", "size": "1.25rem", "name": "Large" },
{ "slug": "xl", "size": "2rem", "name": "XL" }
]
},
"spacing": {
"units": ["rem", "px", "em", "%"],
"spacingScale": { "theme": false },
"spacingSizes": [
{ "slug": "sm", "size": "0.5rem", "name": "Small" },
{ "slug": "md", "size": "1rem", "name": "Medium" },
{ "slug": "lg", "size": "2rem", "name": "Large" }
]
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--primary)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--sans)",
"lineHeight": "1.5"
}
}
}
<?php
/**
* Title: Hero
* Slug: atum/hero
* Categories: featured
*/
?>
<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|lg"}}}} -->
<div class="wp-block-group alignfull">
<!-- wp:heading {"level":1} -->
<h1>Welcome to our agency</h1>
<!-- /wp:heading -->
</div>
<!-- /wp:group -->
my-plugin/
├── my-plugin.php # Fichier principal avec header
├── uninstall.php # Nettoyage à la désinstallation
├── composer.json # Si dépendances PSR-4
├── vendor/ # Composer autoload
├── includes/
│ ├── class-activator.php
│ ├── class-deactivator.php
│ └── class-loader.php
├── admin/
│ └── class-admin.php
├── public/
│ └── class-public.php
├── blocks/ # Blocs Gutenberg du plugin
│ └── my-block/
│ ├── block.json
│ ├── edit.js
│ ├── save.js
│ └── style.css
└── languages/
└── my-plugin.pot
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: Short description under 150 chars.
* Version: 1.0.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: Agency Name
* Author URI: https://agency.example.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
*/
defined('ABSPATH') || exit;
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
dbDeltaregister_activation_hook(__FILE__, function () {
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_log';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
event VARCHAR(64) NOT NULL,
payload LONGTEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY event (event)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option('my_plugin_db_version', MY_PLUGIN_VERSION);
});
uninstall.php<?php
defined('WP_UNINSTALL_PLUGIN') || exit;
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_log';
$wpdb->query("DROP TABLE IF EXISTS {$table}");
delete_option('my_plugin_db_version');
delete_option('my_plugin_settings');
blocks/call-to-action/
├── block.json
├── edit.js
├── save.js
├── style.scss # Style côté front
└── editor.scss # Style côté éditeur uniquement
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "atum/call-to-action",
"version": "1.0.0",
"title": "Call to Action",
"category": "theme",
"icon": "megaphone",
"description": "Section with title, text and button.",
"textdomain": "atum",
"attributes": {
"title": { "type": "string", "default": "Ready?" },
"text": { "type": "string", "default": "Get started today." },
"buttonLabel": { "type": "string", "default": "Sign up" },
"buttonUrl": { "type": "string", "default": "#" }
},
"supports": {
"align": ["wide", "full"],
"color": { "background": true, "text": true },
"spacing": { "padding": true, "margin": ["top", "bottom"] }
},
"editorScript": "file:./edit.js",
"style": "file:./style-index.css",
"editorStyle": "file:./index.css"
}
add_action('init', function () {
register_block_type(__DIR__ . '/blocks/call-to-action');
});
add_action('rest_api_init', function () {
register_rest_route('atum/v1', '/leads', [
[
'methods' => WP_REST_Server::CREATABLE, // POST
'callback' => 'atum_create_lead',
'permission_callback' => '__return_true',
'args' => [
'name' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'email' => [
'required' => true,
'sanitize_callback' => 'sanitize_email',
'validate_callback' => fn($v) => (bool) is_email($v),
],
'message' => [
'required' => false,
'sanitize_callback' => 'wp_kses_post',
],
],
],
]);
});
function atum_create_lead(WP_REST_Request $request): WP_REST_Response
{
$email = $request->get_param('email');
$name = $request->get_param('name');
// Rate limiting simple
$rate_key = 'atum_lead_' . md5($_SERVER['REMOTE_ADDR'] ?? 'unknown');
if (get_transient($rate_key)) {
return new WP_REST_Response(['error' => 'rate_limited'], 429);
}
set_transient($rate_key, 1, MINUTE_IN_SECONDS);
$post_id = wp_insert_post([
'post_type' => 'atum_lead',
'post_title' => $name,
'post_status' => 'private',
'meta_input' => [
'_email' => $email,
'_message' => $request->get_param('message') ?: '',
],
], true);
if (is_wp_error($post_id)) {
return new WP_REST_Response(['error' => 'insert_failed'], 500);
}
return new WP_REST_Response(['id' => $post_id], 201);
}
if (defined('WP_CLI') && WP_CLI) {
WP_CLI::add_command('atum seed', function ($args, $assoc_args) {
$count = (int) ($assoc_args['count'] ?? 10);
WP_CLI::log(sprintf('Seeding %d projects...', $count));
for ($i = 0; $i < $count; $i++) {
wp_insert_post([
'post_type' => 'project',
'post_status' => 'publish',
'post_title' => sprintf('Project %d', $i + 1),
'post_content' => 'Seeded content',
]);
}
WP_CLI::success('Seed done.');
});
}
Usage :
wp atum seed --count=50
// Dans le form
wp_nonce_field('atum_save_settings', '_atum_nonce');
// Dans le handler
if (!isset($_POST['_atum_nonce']) || !wp_verify_nonce($_POST['_atum_nonce'], 'atum_save_settings')) {
wp_die(__('Security check failed.', 'atum'));
}
if (!current_user_can('manage_options')) {
wp_die(__('You are not allowed to do this.', 'atum'));
}
| Type attendu | Fonction |
|---|---|
| Texte libre | sanitize_text_field($value) |
| Textarea | sanitize_textarea_field($value) |
sanitize_email($value) | |
| URL | esc_url_raw($value) |
| Entier positif | absint($value) |
| Slug | sanitize_title($value) |
| HTML autorisé | wp_kses_post($value) |
| HTML restreint | wp_kses($value, $allowed_html) |
| Tableau clé=>val | parcourir et sanitizer chaque élément |
| Contexte | Fonction |
|---|---|
| Texte HTML | esc_html($value) |
| Attribut HTML | esc_attr($value) |
| URL href/src | esc_url($value) |
| JSON dans JS | wp_json_encode($value) |
| Translation + html | esc_html__($text, 'domain') |
| JavaScript inline | esc_js($value) |
Règle absolue : toute variable provenant de la DB, d'une option, d'une meta, d'un champ ACF, d'un paramètre REST ou d'un cookie doit être échappée au point d'injection dans le HTML.
// Textdomain chargé une fois
add_action('init', function () {
load_plugin_textdomain('atum', false, dirname(plugin_basename(__FILE__)) . '/languages');
});
// Utilisation
$message = __('Hello world', 'atum');
_e('Welcome back', 'atum');
echo esc_html__('User count: %d', 'atum');
printf(_n('%d user', '%d users', $count, 'atum'), $count);
mysql_query, $wpdb->query("... $input ...") sans $wpdb->prepare() → SQL injectionecho $_POST['name'] → XSSwp-config.php en runtime → dangereuxfile_get_contents('https://...') au lieu de wp_remote_get() → pas de hooks, pas de cache, pas de timeoutheader('Location: ...') au lieu de wp_safe_redirect() → pas de whitelistwp_hash_password() obligatoirecreate_function, PHP exec functions) → banniswordpress-expert (dans ce plugin)woocommerce-patterns (dans ce plugin)security-reviewer d'atum-reviewersmcp__context7__resolve-library-id puis query-docs