Un client vendait des rendez-vous de conseil et voulait un formulaire de prise de contact directement intégré comme bloc, réutilisable sur plusieurs pages, sans dépendre d’une extension tierce de formulaires. Le cahier des charges tenait en une phrase : « un champ nom, un champ e-mail, un message, et ça doit fonctionner sans rechargement de page ».
Ce cas d’usage est un bon prétexte pour construire, de bout en bout, un bloc front qui collecte des données, les valide, puis les envoie à une route REST personnalisée via fetch et l’API apiFetch de WordPress. Voici comment on l’a structuré, avec les pièges rencontrés en cours de route.
Déclarer la route REST côté serveur
La route s’enregistre classiquement avec register_rest_route() sur le hook rest_api_init. Le point à ne pas négliger : le callback de permission. Un formulaire de contact public doit rester accessible aux visiteurs non connectés, donc permission_callback renvoie true, mais cela ne dispense pas de vérifier le nonce transmis dans l’en-tête pour se protéger du CSRF, ni de valider et d’assainir chaque champ reçu.
add_action( 'rest_api_init', function () {
register_rest_route(
'agence-contact/v1',
'/message',
array(
'methods' => 'POST',
'callback' => 'agence_contact_traiter_message',
'permission_callback' => '__return_true',
'args' => array(
'nom' => array( 'required' => true, 'type' => 'string' ),
'email' => array( 'required' => true, 'type' => 'string' ),
'message' => array( 'required' => true, 'type' => 'string' ),
),
)
);
} );
function agence_contact_traiter_message( WP_REST_Request $request ) {
$email = sanitize_email( $request->get_param( 'email' ) );
if ( ! is_email( $email ) ) {
return new WP_Error( 'email_invalide', 'Adresse e-mail invalide.', array( 'status' => 400 ) );
}
$nom = sanitize_text_field( $request->get_param( 'nom' ) );
$message = sanitize_textarea_field( $request->get_param( 'message' ) );
wp_mail(
get_option( 'admin_email' ),
sprintf( 'Nouveau message de %s', $nom ),
$message,
array( 'Reply-To: ' . $email )
);
return new WP_REST_Response( array( 'statut' => 'envoye' ), 200 );
}
Le nonce, sans surcouche
Le point qui bloque le plus souvent les débutants : WordPress fournit déjà un nonce prêt à l’emploi pour l’API REST via wp_create_nonce( 'wp_rest' ), à condition de le rendre disponible côté front. Sur le front public (pas dans l’éditeur), on le sort proprement avec wp_localize_script() ou wp_add_inline_script() attaché au view.js du bloc, jamais en dur dans le HTML statique du save() car cela figerait un nonce périmé dans le cache de page.
function agence_contact_enregistrer_nonce() {
wp_add_inline_script(
'agence-contact-view-script',
'window.agenceContactNonce = ' . wp_json_encode( wp_create_nonce( 'wp_rest' ) ) . ';',
'before'
);
}
add_action( 'wp_enqueue_scripts', 'agence_contact_enregistrer_nonce' );

Validation front avant l’envoi
La validation côté client sert l’expérience utilisateur, pas la sécurité : elle évite un aller-retour réseau inutile pour une erreur de saisie évidente, mais elle ne remplace jamais la validation serveur vue plus haut. Un visiteur peut toujours désactiver JavaScript ou appeler la route directement avec curl.
async function envoyerFormulaire( evenement ) {
evenement.preventDefault();
const form = evenement.target;
const email = form.querySelector( '[name="email"]' ).value;
if ( ! /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( email ) ) {
afficherErreur( form, 'email', 'Vérifiez le format de votre adresse e-mail.' );
return;
}
const reponse = await fetch( '/wp-json/agence-contact/v1/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.agenceContactNonce,
},
body: JSON.stringify( {
nom: form.querySelector( '[name="nom"]' ).value,
email,
message: form.querySelector( '[name="message"]' ).value,
} ),
} );
if ( reponse.ok ) {
afficherConfirmation( form );
} else {
const erreur = await reponse.json();
afficherErreur( form, 'general', erreur.message );
}
}
Rendre les erreurs perceptibles
Un message d’erreur qui apparaît visuellement mais que personne n’annonce à un lecteur d’écran n’aide qu’une partie des visiteurs. La fonction afficherErreur() associe le message au champ via aria-describedby, et le conteneur global des erreurs porte aria-live="polite" pour que son apparition soit lue automatiquement, sans déplacer le focus de force.
- Chaque champ en erreur reçoit
aria-invalid="true"le temps que l’utilisateur corrige. - Le message d’erreur est un élément avec un
idunique, référencé pararia-describedbysur le champ. - La confirmation d’envoi réussi est elle aussi annoncée via une zone
aria-live, pas seulement un changement de couleur.
Sur ce projet, tester le formulaire au clavier seul, sans souris, a révélé que le focus restait sur le bouton « Envoyer » après une erreur — l’utilisateur ne savait pas où regarder. On a ajouté un déplacement de focus explicite vers le premier champ en erreur, ce qui a réglé le problème en une ligne de JavaScript.
En résumé
Un bloc de formulaire front autonome tient en trois briques solidaires : une route REST qui ne fait confiance à rien côté serveur, un nonce distribué proprement sans le figer dans le HTML mis en cache, et une couche d’accessibilité qui traite les erreurs comme des informations à annoncer, pas seulement à afficher. Le tout sans dépendance à une extension tierce, pour un formulaire de trois champs qui n’en avait pas besoin.