Essayez sans attendre l'hébergement proposé par WordPress
-15% sur le premier mois avec le code 2025PRESS15AFF

Essayer maintenant

Personnaliser la Command Palette de WordPress (Ctrl+K) pour son propre plugin

Ctrl+K, ce petit raccourci qui transforme l’administration WordPress en véritable centre de commandes, ne se limite pas aux actions natives du core. Bonne nouvelle : depuis WordPress 6.3, l’API JavaScript permet d’y injecter ses propres commandes, y compris une recherche instantanée sur un Custom Post Type. On va voir ensemble comment exploiter registerCommand et useCommandLoader pour offrir à vos utilisateurs une navigation ultra-rapide dans la Command Palette, digne des meilleurs plugins pro.

Comprendre la Command Palette et son API JavaScript

WordPress 6.3 a introduit une fonctionnalité assez discrète mais franchement pratique : la Command Palette. Un raccourci clavier (Ctrl+K sous Windows/Linux, Cmd+K sous Mac) et hop, une petite fenêtre de recherche s’ouvre pour naviguer rapidement dans l’admin. Si vous avez déjà utilisé Notion, Slack ou VS Code, le concept vous parlera immédiatement : c’est le même principe que Spotlight sur Mac, mais appliqué à WordPress. L’idée, c’est d’éviter les allers-retours fastidieux dans les menus pour créer un article, changer de thème ou accéder aux réglages. Pratique, non ? Et surtout, cette palette est entièrement extensible via JavaScript : on peut donc y greffer ses propres commandes pour son plugin.

À quoi sert registerCommand ?

registerCommand() est la fonction centrale de cette API. Elle permet d’ajouter une nouvelle entrée dans la Command Palette, avec un comportement personnalisé au clic. Concrètement, dès qu’un utilisateur tape quelques lettres dans la palette, WordPress filtre les commandes disponibles et affiche les correspondances. Si votre plugin gère, par exemple, des réservations ou des produits, vous pouvez ajouter une commande du type « Créer une nouvelle réservation » directement accessible depuis n’importe quel écran de l’admin. Ça évite à l’utilisateur de chercher dans les menus, et ça donne une impression de fluidité assez appréciable niveau UX.

Les packages @wordpress/commands et @wordpress/commands-store

Techniquement, tout repose sur deux packages : @wordpress/commands (l’API publique, avec les hooks et fonctions qu’on va utiliser) et @wordpress/commands-store (le store Redux qui gère l’état interne des commandes enregistrées). Pas besoin d’installer quoi que ce soit via npm si vous travaillez directement dans l’admin WordPress : ces packages sont déjà chargés et accessibles globalement via wp.commands. Si votre wp_enqueue_script() déclare bien la dépendance wp-commands, vous avez accès à toutes les fonctions nécessaires sans configuration supplémentaire. C’est quand même bien pensé.

Anatomie d’une commande (name, label, icon, callback)

Une commande, c’est un objet JavaScript avec quelques propriétés clés :

  • name : un identifiant unique, généralement préfixé par le nom de votre plugin (ex : monplugin/ma-commande). Ça évite les collisions avec d’autres extensions.
  • label : le texte affiché dans la palette, celui que l’utilisateur va lire et rechercher.
  • icon : une icône visuelle, issue de @wordpress/icons ou d’un dashicon classique (pratique pour rester cohérent avec le design de l’admin).
  • callback : la fonction exécutée quand l’utilisateur clique sur la commande (redirection, ouverture d’une modale, déclenchement d’une action, etc.).
  • context (optionnel) : permet de limiter l’affichage de la commande à certains écrans seulement, histoire de ne pas polluer la palette partout.

L’enregistrement se fait via wp.data.dispatch('core/commands').registerCommand({...}), en passant directement l’objet avec ces propriétés.

Les hooks associés : useCommand et useCommandLoader

Si vous développez avec des composants React (ce qui est le cas dès qu’on touche à l’éditeur de blocs ou aux écrans modernes de l’admin), deux hooks facilitent grandement les choses. useCommand permet d’enregistrer une commande directement depuis un composant, avec un cycle de vie propre (la commande s’enregistre au montage et se désenregistre au démontage, pas de fuite mémoire à gérer manuellement). useCommandLoader, lui, sert à charger dynamiquement une liste de commandes, typiquement utile pour la recherche contextuelle : imaginez rechercher un article par son titre directement depuis la palette, sans avoir à enregistrer chaque article en dur. Ces deux hooks complètent bien l’API de base et permettent des intégrations plus poussées.

Créer un plugin avec des commandes personnalisées

Maintenant qu’on a vu la théorie, passons à la pratique ! Rien de tel qu’un exemple concret pour comprendre comment tout ça s’articule. On va créer un mini plugin qui ajoute trois commandes utiles à la Command Palette : créer un article, vider le cache, et accéder aux réglages. De quoi couvrir tous les cas de figure classiques.

Mise en place du plugin et enqueue du script

Première étape : la structure du plugin. Rien d’exotique ici, on reste sur du classique.

mon-plugin-commands/
├── mon-plugin-commands.php
└── build/
    └── commands.js

Dans le fichier PHP principal, on déclare l’en-tête du plugin puis on enqueue notre script JS. Point important : la dépendance wp-commands est obligatoire, sinon wp.commands n’existera tout simplement pas au moment de l’exécution.

<?php
/**
 * Plugin Name: Mon Plugin Commands
 * Description: Ajoute des commandes personnalisées à la Command Palette
 * Version: 1.0
 */

function mpc_enqueue_commands_script() {
    wp_enqueue_script(
        'mpc-commands',
        plugin_dir_url( __FILE__ ) . 'build/commands.js',
        array( 'wp-commands', 'wp-i18n', 'wp-icons', 'wp-url' ),
        '1.0',
        true
    );
}
add_action( 'admin_enqueue_scripts', 'mpc_enqueue_commands_script' );

Notez qu’on charge le script uniquement dans l’admin via admin_enqueue_scripts : la Command Palette n’a pas vocation à exister en frontend (pour l’instant du moins).

Enregistrer une commande simple avec icône et raccourci

Passons au JavaScript. On utilise wp.commands.registerCommand pour ajouter notre première commande : créer un nouvel article. L’icône rend la palette bien plus lisible, donc autant en profiter (WordPress fournit un paquet @wordpress/icons bien fourni).

( function( wp ) {
    const { registerCommand } = wp.commands;
    const { plus } = wp.icons;
    const { __ } = wp.i18n;

    registerCommand( {
        name: 'mon-plugin/creer-article',
        label: __( 'Créer un nouvel article', 'mon-plugin' ),
        icon: plus,
        callback: () => {
            window.location.href = '/wp-admin/post-new.php';
        },
    } );
} )( window.wp );

Vous pouvez aussi ajouter un raccourci clavier optionnel via la propriété shortcut (attention, il doit rester cohérent avec les raccourcis déjà utilisés par WordPress pour éviter les conflits) :

registerCommand( {
    name: 'mon-plugin/creer-article',
    label: __( 'Créer un nouvel article', 'mon-plugin' ),
    icon: plus,
    callback: () => window.location.href = '/wp-admin/post-new.php',
    shortcut: 'ctrl+alt+n', // optionnel, mais pratique pour les power users
} );

Ajouter plusieurs commandes regroupées (groupe personnalisé)

Dès qu’on ajoute plus d’une commande, il devient intéressant de les regrouper sous un même namespace. Ça facilite la lecture pour l’utilisateur (et évite les collisions de noms avec d’autres plugins). On préfixe simplement toutes nos commandes avec le même identifiant :

( function( wp ) {
    const { registerCommand } = wp.commands;
    const { trash, cog, plus } = wp.icons;
    const { __ } = wp.i18n;

    // Commande 1 : créer un article
    registerCommand( {
        name: 'mon-plugin/creer-article',
        label: __( 'Créer un nouvel article', 'mon-plugin' ),
        icon: plus,
        callback: () => window.location.href = '/wp-admin/post-new.php',
    } );

    // Commande 2 : vider le cache
    registerCommand( {
        name: 'mon-plugin/vider-cache',
        label: __( 'Vider le cache', 'mon-plugin' ),
        icon: trash,
        callback: ( { close } ) => {
            fetch( '/wp-json/mon-plugin/v1/clear-cache', { method: 'POST' } )
                .then( () => close() );
        },
    } );

    // Commande 3 : réglages du plugin
    registerCommand( {
        name: 'mon-plugin/reglages',
        label: __( 'Aller aux réglages du plugin', 'mon-plugin' ),
        icon: cog,
        callback: () => window.location.href = '/wp-admin/admin.php?page=mon-plugin-settings',
    } );
} )( window.wp );

Le préfixe mon-plugin/ fait office de groupe implicite : dans la palette, l’utilisateur verra rapidement que ces trois commandes viennent de la même source. Simple, mais diablement efficace pour l’expérience utilisateur !

Rendre une commande contextuelle selon l’écran d’administration

Dernier point, et non des moindres : toutes les commandes n’ont pas de sens partout. Vider le cache, par exemple, reste pertinent sur n’importe quel écran. Par contre, une commande spécifique à un CPT (custom post type) n’a aucun intérêt en dehors de son contexte d’édition.

Pour ça, deux approches possibles. La première consiste à vérifier l’écran actuel directement dans le callback ou avant l’enregistrement, via wp.data.select( 'core/edit-post' ) :

const currentPostType = wp.data.select( 'core/editor' )?.getCurrentPostType();

if ( currentPostType === 'produit' ) {
    registerCommand( {
        name: 'mon-plugin/dupliquer-produit',
        label: __( 'Dupliquer ce produit', 'mon-plugin' ),
        icon: wp.icons.copy,
        callback: ( { close } ) => {
            // logique de duplication ici
            close();
        },
    } );
}

La seconde approche, plus simple pour des cas basiques, consiste à checker window.location (utile si vous n’êtes pas dans l’éditeur de blocs) :

if ( window.location.href.includes( 'post_type=produit' ) ) {
    registerCommand( {
        name: 'mon-plugin/export-produit',
        label: __( 'Exporter cette fiche produit', 'mon-plugin' ),
        icon: wp.icons.download,
        callback: () => { /* export logic */ },
    } );
}

Attention cependant : ce genre de vérification doit s’exécuter à chaque chargement de page (le script étant rechargé à chaque navigation admin classique), donc pas besoin de gérer un état réactif complexe ici. Si vous développez dans un contexte React avec useCommand (comme vu en introduction), vous pouvez conditionner l’appel du hook directement selon l’écran actif, ce qui est plus propre pour les cas dynamiques (SPA type éditeur de site).

Cas d’usage : recherche rapide de contenus personnalisés

Passons maintenant à un exemple concret, celui qui m’a le plus servi sur mes projets clients : ajouter à la Command Palette une recherche instantanée sur un Custom Post Type. Imaginons qu’on gère un CPT « Produits » (ou « Événements », le principe reste identique). L’objectif : taper Ctrl+K, écrire quelques lettres, et voir apparaître directement les fiches produits correspondantes, sans quitter l’écran en cours. Franchement, une fois qu’on a goûté à ce confort, difficile de s’en passer !

Charger dynamiquement des résultats avec useCommandLoader

Contrairement à registerCommand qui définit une liste statique, useCommandLoader permet de générer des commandes à la volée, en fonction de la saisie de l’utilisateur. C’est exactement ce qu’il nous faut ici : le hook reçoit un paramètre search (le texte tapé dans la palette) et doit retourner un tableau de commandes recalculé à chaque frappe.

Concrètement, on l’utilise ainsi :

useCommandLoader( {
    name: 'mon-plugin/rechercher-produits',
    hook: useProduitsCommandLoader,
} );

Le hook useProduitsCommandLoader reçoit search en argument et se charge d’aller chercher les données. C’est là que ça devient intéressant : on ne charge rien tant que l’utilisateur n’a rien tapé (évite des requêtes inutiles au chargement de la palette).

Appeler l’API REST WordPress pour rechercher un CPT

Pour récupérer les résultats, on interroge simplement l’API REST native de WordPress. Si votre CPT « produits » expose bien son endpoint (option show_in_rest activée dans register_post_type), une requête suffit :

const response = await fetch(
    `/wp-json/wp/v2/produits?search=${ encodeURIComponent( search ) }&per_page=5`
);
const produits = await response.json();

On transforme ensuite chaque résultat en objet commande exploitable par la palette :

const commands = produits.map( ( produit ) => ( {
    name: `produit-${ produit.id }`,
    label: produit.title.rendered,
    icon: 'cart',
    callback: () => {
        document.location.href = `/wp-admin/post.php?post=${ produit.id }&action=edit`;
    },
} ) );

Le label reprend le titre du contenu, icon peut varier selon le type (un panier pour un produit, un calendrier pour un événement), et le callback redirige vers l’écran d’édition.

Naviguer directement vers le contenu trouvé

Une fois le résultat sélectionné, deux options s’offrent à vous pour la navigation : window.location.href (rechargement complet, simple et fiable) ou history.push si vous travaillez dans un contexte React Router côté admin. Dans la pratique, document.location.href reste largement suffisant pour rediriger vers l’écran d’édition classique de WordPress.

Quelques bonnes pratiques indispensables, apprises à mes dépens :

  • Debounce des requêtes : ne lancez pas un fetch à chaque frappe, attendez 300 à 400ms d’inactivité (sinon, vous allez saturer l’API et l’expérience utilisateur en pâtit).
  • Gestion du cache : mémorisez les résultats déjà récupérés pour une même recherche, ça évite des appels redondants.
  • Limitation du nombre de résultats : 5 à 10 suffisent largement (per_page=5 dans l’exemple ci-dessus). Personne n’a envie de scroller dans une palette de commandes !

Bref, avec ces quelques précautions, on obtient une recherche fluide, rapide, et parfaitement intégrée à l’écosystème WordPress natif.