Bon, je vais être franc avec vous : créer un système de notifications push pour WordPress, c’est un peu comme monter un moteur de voiture… ça paraît complexe au premier regard, mais une fois qu’on comprend l’architecture, tout devient logique ! Les notifications push natives offrent une expérience utilisateur bien supérieure aux plugins externes (pas de dépendance tierce, performances optimales). Dans cet article, on va construire ensemble un système complet, des clés VAPID jusqu’à l’interface d’administration, sans passer par des services externes coûteux.
Architecture et prérequis du système de notifications push
Maintenant qu’on a vu l’intérêt des notifications push, rentrons dans le vif du sujet. Développer un système natif dans WordPress, c’est s’assurer d’avoir le contrôle total sur ses fonctionnalités. Et croyez-moi, après avoir testé plusieurs plugins tiers qui ne répondaient pas exactement à mes besoins, j’ai vite compris l’importance de cette approche !
Technologies nécessaires et compatibilité
Pour créer notre système de notifications push, on va s’appuyer sur des technologies web modernes. D’abord, il faut comprendre qu’on aura besoin de la Service Worker API et de la Push API côté navigateur. Ces APIs sont maintenant bien supportées : Chrome 42+, Firefox 44+, et enfin Safari 16+ (mieux vaut tard que jamais !).
Côté serveur, il nous faut également les clés VAPID (Voluntary Application Server Identification). Ces clés permettent d’identifier votre serveur auprès des services de push des navigateurs. Pas d’inquiétude, on verra comment les générer facilement.
Un point crucial : HTTPS est obligatoire. Sans SSL, les Service Workers ne fonctionnent tout simplement pas. Et côté PHP, on aura besoin d’au moins la version 7.4+ pour une gestion optimale des requêtes HTTP/2.
Service Worker et Push API
Le Service Worker, c’est le cœur de notre système. Il s’agit d’un script JavaScript qui s’exécute en arrière-plan, même quand l’utilisateur n’est pas sur votre site. C’est lui qui va intercepter les notifications push et les afficher.
La Push API, elle, permet au navigateur de recevoir des messages depuis votre serveur. Ces deux technologies travaillent ensemble : le Service Worker s’enregistre pour recevoir les notifications, et la Push API gère la communication avec les serveurs de push (Google FCM pour Chrome, Mozilla Push Service pour Firefox, etc.).
Bon, je vais être honnête : la première fois que j’ai implémenté ça, j’ai eu du mal à comprendre le flux. Mais une fois qu’on saisit le principe (inscription → stockage → envoi → réception → affichage), tout devient plus clair.
Structure de la base de données
Pour stocker les subscriptions des utilisateurs, on va créer une table dédiée : wp_push_subscriptions. Voici la structure que j’utilise :
id: clé primaire auto-incrémentéeuser_id: ID de l’utilisateur WordPress (peut être 0 pour les visiteurs anonymes)endpoint: URL unique fournie par le navigateurp256dh_key: clé de chiffrement pour sécuriser les donnéesauth_key: clé d’authentificationuser_agent: pour identifier le navigateur/appareilcreated_at: timestamp de création
Cette structure nous permet de gérer plusieurs subscriptions par utilisateur (desktop, mobile, différents navigateurs). Et c’est important car un utilisateur peut très bien avoir WordPress ouvert sur son téléphone ET son ordinateur !
Attention cependant : ces données sont sensibles. Il faut absolument les protéger et prévoir un système de nettoyage des subscriptions expirées (les endpoints peuvent changer).
Génération et gestion des clés VAPID
Bon, maintenant qu’on a posé les bases de notre architecture, on va attaquer le gros morceau : les clés VAPID. Et croyez-moi, c’est un point crucial qu’on ne peut pas négliger !
Qu’est-ce que VAPID et pourquoi c’est indispensable
VAPID (Voluntary Application Server Identification), c’est en quelque sorte la carte d’identité de votre serveur auprès des services de push. Sans ces clés, impossible d’envoyer des notifications ! Le principe ? Générer une paire de clés cryptographiques (publique/privée) qui permettent d’authentifier vos requêtes.
La clé publique sera partagée avec le navigateur lors de la subscription, tandis que la clé privée reste secrète sur votre serveur. C’est elle qui signe vos requêtes pour prouver que vous êtes bien autorisé à envoyer des notifications à vos utilisateurs.
Installation de la librairie web-push-php
Pour générer nos clés VAPID, on va utiliser la librairie web-push/web-push-php. C’est LA référence pour les notifications push en PHP, et elle gère tout ça très bien.
composer require web-push/web-push-php
Une fois installée, on peut commencer à générer nos précieuses clés. La librairie fournit tout ce qu’il faut pour créer des clés conformes aux standards.
Génération automatique des clés VAPID
Voici une fonction WordPress pour générer automatiquement les clés VAPID au premier lancement de votre plugin :
use Minishlink\WebPush\VAPID;
function generate_vapid_keys() {
// Vérifier si les clés existent déjà
if (get_option('vapid_public_key') && get_option('vapid_private_key')) {
return false; // Clés déjà générées
}
try {
// Générer les clés VAPID
$keys = VAPID::createVapidKeys();
// Stocker les clés
update_option('vapid_public_key', $keys['publicKey']);
update_option('vapid_private_key', $keys['privateKey']);
update_option('vapid_generated_at', time());
return $keys;
} catch (Exception $e) {
error_log('Erreur génération VAPID: ' . $e->getMessage());
return false;
}
}
// Hook d'activation du plugin
register_activation_hook(__FILE__, 'generate_vapid_keys');
Attention cependant : cette génération ne doit se faire qu’une seule fois ! Si vous régénérez les clés, toutes les subscriptions existantes deviennent inutilisables.
Stockage sécurisé des clés
Bon, maintenant qu’on a nos clés, où les stocker ? Deux options s’offrent à nous :
Option 1 : Dans wp_options (plus simple)
function get_vapid_keys() {
return [
'public' => get_option('vapid_public_key'),
'private' => get_option('vapid_private_key')
];
}
Option 2 : En constantes dans wp-config.php (plus sécurisé)
// Dans wp-config.php
define('VAPID_PUBLIC_KEY', 'votre_clé_publique_ici');
define('VAPID_PRIVATE_KEY', 'votre_clé_privée_ici');
// Dans votre plugin
function get_vapid_keys() {
return [
'public' => VAPID_PUBLIC_KEY,
'private' => VAPID_PRIVATE_KEY
];
}
Personnellement, je préfère la méthode des constantes pour la production : c’est plus difficile à compromettre et ça évite les fuites par la base de données.
Fonction d’initialisation complète
Voici une fonction plus robuste qui gère l’initialisation et la vérification des clés :
function init_vapid_system() {
// Vérifier la présence des clés
$public_key = defined('VAPID_PUBLIC_KEY') ? VAPID_PUBLIC_KEY : get_option('vapid_public_key');
$private_key = defined('VAPID_PRIVATE_KEY') ? VAPID_PRIVATE_KEY : get_option('vapid_private_key');
if (!$public_key || !$private_key) {
// Générer les clés manquantes
$keys = generate_vapid_keys();
if (!$keys) {
wp_die('Impossible de générer les clés VAPID');
}
// Logger la génération
error_log('Nouvelles clés VAPID générées');
}
// Valider le format des clés
if (!validate_vapid_keys($public_key, $private_key)) {
wp_die('Clés VAPID invalides');
}
return true;
}
function validate_vapid_keys($public, $private) {
// Vérifications basiques
if (strlen($public) !== 88 || strlen($private) !== 43) {
return false;
}
// Plus de validations si nécessaire
return true;
}
Rotation et sécurité des clés
Pour la sécurité, il faut penser à la rotation des clés. Même si ce n’est pas obligatoire, c’est une bonne pratique :
function should_rotate_vapid_keys() {
$generated_at = get_option('vapid_generated_at', 0);
$six_months = 6 * MONTH_IN_SECONDS;
return (time() - $generated_at) > $six_months;
}
function rotate_vapid_keys() {
if (!current_user_can('manage_options')) {
return false;
}
// Sauvegarder les anciennes clés
$old_keys = get_vapid_keys();
update_option('vapid_old_public_key', $old_keys['public']);
// Générer nouvelles clés
delete_option('vapid_public_key');
delete_option('vapid_private_key');
return generate_vapid_keys();
}
Attention : la rotation des clés implique de relancer une campagne de subscription pour tous vos utilisateurs !
Implémentation côté frontend et Service Worker
Maintenant qu’on a mis en place nos clés VAPID, on va pouvoir s’attaquer au côté frontend. Et c’est là que ça devient intéressant : on va créer tout l’écosystème JavaScript qui permettra à nos utilisateurs de recevoir les notifications.
Enregistrement du Service Worker
Première étape : créer et enregistrer notre Service Worker. Ce fichier va tourner en arrière-plan et gérer les notifications même quand l’utilisateur n’est pas sur notre site.
Créons d’abord le fichier sw.js à la racine de notre thème :
// sw.js
self.addEventListener('push', function(event) {
if (!(self.Notification && self.Notification.permission === 'granted')) {
return;
}
const data = event.data ? event.data.json() : {};
const title = data.title || 'Nouvelle notification';
const options = {
body: data.body || 'Vous avez une nouvelle notification',
icon: data.icon || '/wp-content/themes/votre-theme/assets/icon-192.png',
badge: data.badge || '/wp-content/themes/votre-theme/assets/badge-72.png',
data: data.url || '/',
actions: [
{
action: 'view',
title: 'Voir',
icon: '/wp-content/themes/votre-theme/assets/view-icon.png'
},
{
action: 'close',
title: 'Fermer'
}
]
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'view') {
event.waitUntil(
clients.openWindow(event.notification.data)
);
}
});
Ensuite, on enregistre ce Service Worker dans notre fichier JavaScript principal :
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/wp-content/themes/votre-theme/sw.js')
.then(function(registration) {
console.log('Service Worker enregistré:', registration);
return registration;
})
.catch(function(error) {
console.error('Erreur Service Worker:', error);
});
} else {
console.warn('Push notifications non supportées');
}
Gestion des subscriptions utilisateur
Bon, maintenant on va gérer les subscriptions. C’est le cœur du système : on va créer une fonction qui s’occupe de tout.
class PushNotificationManager {
constructor() {
this.vapidPublicKey = 'VOTRE_CLE_PUBLIQUE_VAPID'; // À récupérer depuis PHP
this.registration = null;
this.isSubscribed = false;
}
async init() {
try {
this.registration = await navigator.serviceWorker.ready;
const subscription = await this.registration.pushManager.getSubscription();
this.isSubscribed = !(subscription === null);
this.updateUI();
} catch (error) {
console.error('Erreur initialisation:', error);
}
}
async subscribe() {
try {
const subscription = await this.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
});
// Envoyer la subscription au serveur
await this.sendSubscriptionToServer(subscription);
this.isSubscribed = true;
this.updateUI();
return subscription;
} catch (error) {
console.error('Erreur subscription:', error);
throw error;
}
}
async unsubscribe() {
try {
const subscription = await this.registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
await this.removeSubscriptionFromServer(subscription);
}
this.isSubscribed = false;
this.updateUI();
} catch (error) {
console.error('Erreur unsubscribe:', error);
}
}
async sendSubscriptionToServer(subscription) {
const response = await fetch(ajax_object.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'save_push_subscription',
nonce: ajax_object.nonce,
subscription: JSON.stringify(subscription)
})
});
if (!response.ok) {
throw new Error('Erreur serveur lors de la sauvegarde');
}
return response.json();
}
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
Interface d’autorisation
Maintenant, créons une interface sympa pour que l’utilisateur puisse activer ou désactiver les notifications. On va faire ça proprement avec une belle UI :
<div id="push-notification-container" class="push-notification-ui">
<div class="notification-prompt" id="notification-prompt">
<div class="prompt-content">
<h3>Notifications push</h3>
<p>Restez informé des dernières actualités directement dans votre navigateur.</p>
<div class="prompt-actions">
<button id="enable-notifications" class="btn btn-primary">
Activer les notifications
</button>
<button id="maybe-later" class="btn btn-secondary">
Plus tard
</button>
</div>
</div>
</div>
<div class="notification-status" id="notification-status" style="display: none;">
<span class="status-text" id="status-text"></span>
<button id="toggle-notifications" class="btn"></button>
</div>
</div>
Et le JavaScript qui va avec :
const pushManager = new PushNotificationManager();
document.addEventListener('DOMContentLoaded', function() {
const enableBtn = document.getElementById('enable-notifications');
const toggleBtn = document.getElementById('toggle-notifications');
const maybeLaterBtn = document.getElementById('maybe-later');
pushManager.updateUI = function() {
const prompt = document.getElementById('notification-prompt');
const status = document.getElementById('notification-status');
const statusText = document.getElementById('status-text');
const toggleBtn = document.getElementById('toggle-notifications');
if (Notification.permission === 'default') {
prompt.style.display = 'block';
status.style.display = 'none';
} else {
prompt.style.display = 'none';
status.style.display = 'block';
if (this.isSubscribed) {
statusText.textContent = 'Notifications activées';
toggleBtn.textContent = 'Désactiver';
toggleBtn.className = 'btn btn-danger';
} else {
statusText.textContent = 'Notifications désactivées';
toggleBtn.textContent = 'Activer';
toggleBtn.className = 'btn btn-primary';
}
}
};
enableBtn.addEventListener('click', async function() {
try {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
await pushManager.subscribe();
} else {
alert('Permission refusée. Vous pouvez la modifier dans les paramètres de votre navigateur.');
}
} catch (error) {
console.error('Erreur activation:', error);
alert('Une erreur est survenue lors de l\'activation.');
}
});
toggleBtn.addEventListener('click', async function() {
try {
if (pushManager.isSubscribed) {
await pushManager.unsubscribe();
} else {
await pushManager.subscribe();
}
} catch (error) {
console.error('Erreur toggle:', error);
}
});
maybeLaterBtn.addEventListener('click', function() {
document.getElementById('notification-prompt').style.display = 'none';
// On peut stocker cette info pour ne pas reproposer tout de suite
localStorage.setItem('push-prompt-dismissed', Date.now());
});
// Initialiser le manager
pushManager.init();
});
Attention : cette implémentation gère tous les cas d’usage, mais n’oubliez pas de tester sur différents navigateurs. Chrome et Firefox ont parfois des comportements légèrement différents !
Interface d’administration WordPress et troubleshooting
Maintenant qu’on a notre système de notifications opérationnel côté frontend, il faut créer une interface d’administration digne de ce nom. Et croyez-moi, après avoir développé plusieurs systèmes similaires, une bonne interface admin fait toute la différence !
Panel d’administration des notifications
Pour créer notre interface, on va ajouter une page dans le menu « Réglages » de WordPress. Voici le code principal :
add_action('admin_menu', 'push_notifications_admin_menu');
function push_notifications_admin_menu() {
add_options_page(
'Notifications Push',
'Push Notifications',
'manage_options',
'push-notifications',
'push_notifications_admin_page'
);
}
function push_notifications_admin_page() {
global $wpdb;
// Statistiques rapides
$total_subscriptions = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}push_subscriptions WHERE active = 1"
);
echo '<div class="wrap">';
echo '<h1>Gestion des Notifications Push</h1>';
echo '<div class="notice notice-info"><p>Subscriptions actives : ' . $total_subscriptions . '</p></div>';
// Affichage des onglets
$current_tab = isset($_GET['tab']) ? $_GET['tab'] : 'dashboard';
push_notifications_admin_tabs($current_tab);
switch($current_tab) {
case 'send':
push_notifications_send_form();
break;
case 'logs':
push_notifications_logs_page();
break;
default:
push_notifications_dashboard();
}
echo '</div>';
}
J’ai intégré un système d’onglets pour organiser les différentes fonctionnalités. Le dashboard principal affiche les statistiques essentielles : nombre de subscriptions, dernières notifications envoyées, et éventuellement des graphiques si vous voulez pousser plus loin.
Envoi de notifications en masse
La partie envoi mérite une attention particulière. On veut pouvoir prévisualiser nos notifications avant de les envoyer :
function push_notifications_send_form() {
if (isset($_POST['send_notification'])) {
$title = sanitize_text_field($_POST['notification_title']);
$body = sanitize_textarea_field($_POST['notification_body']);
$url = esc_url_raw($_POST['notification_url']);
$icon = esc_url_raw($_POST['notification_icon']);
// Envoi en arrière-plan pour éviter les timeouts
wp_schedule_single_event(time(), 'send_push_notification_batch', [
'title' => $title,
'body' => $body,
'url' => $url,
'icon' => $icon
]);
echo '<div class="notice notice-success"><p>Notification programmée pour envoi !</p></div>';
}
?>
<form method="post" id="push-notification-form">
<table class="form-table">
<tr>
<th><label for="notification_title">Titre</label></th>
<td><input type="text" id="notification_title" name="notification_title" class="regular-text" required /></td>
</tr>
<tr>
<th><label for="notification_body">Message</label></th>
<td><textarea id="notification_body" name="notification_body" rows="3" cols="50" required></textarea></td>
</tr>
<tr>
<th><label for="notification_url">URL de destination</label></th>
<td><input type="url" id="notification_url" name="notification_url" class="regular-text" /></td>
</tr>
<tr>
<th><label for="notification_icon">Icône</label></th>
<td><input type="url" id="notification_icon" name="notification_icon" class="regular-text" placeholder="<?php echo get_site_icon_url(); ?>" /></td>
</tr>
</table>
<div id="notification-preview" style="margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; display: none;">
<h4>Aperçu de la notification :</h4>
<div id="preview-content"></div>
</div>
<?php submit_button('Envoyer la notification', 'primary', 'send_notification'); ?>
</form>
<script>
// Prévisualisation en temps réel
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('push-notification-form');
const preview = document.getElementById('notification-preview');
const previewContent = document.getElementById('preview-content');
function updatePreview() {
const title = document.getElementById('notification_title').value;
const body = document.getElementById('notification_body').value;
if (title || body) {
preview.style.display = 'block';
previewContent.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px;">
<img src="${document.getElementById('notification_icon').value || '<?php echo get_site_icon_url(); ?>'}" width="24" height="24" />
<div>
<strong>${title}</strong><br>
<span>${body}</span>
</div>
</div>
`;
} else {
preview.style.display = 'none';
}
}
form.addEventListener('input', updatePreview);
});
</script>
<?php
}
Pour l’envoi en masse, j’utilise wp_schedule_single_event() pour éviter les timeouts. Ça permet de traiter les envois en arrière-plan, ce qui est crucial quand on a des milliers de subscriptions.
Résolution des problèmes courants
Bon, on arrive à la partie que tout développeur redoute : le debugging ! Voici les problèmes les plus fréquents et leurs solutions :
Problème HTTPS : Les notifications push ne fonctionnent qu’en HTTPS (sauf sur localhost). Si votre site est en HTTP, c’est mort :
function check_https_requirement() {
if (!is_ssl() && $_SERVER['HTTP_HOST'] !== 'localhost') {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>Les notifications push nécessitent HTTPS !</p></div>';
});
return false;
}
return true;
}
Clés VAPID invalides : Souvent, le problème vient d’un mauvais format ou d’une clé corrompue. J’ai créé une fonction de test :
function test_vapid_keys() {
$public_key = get_option('push_vapid_public_key');
$private_key = get_option('push_vapid_private_key');
if (empty($public_key) || empty($private_key)) {
return ['status' => 'error', 'message' => 'Clés VAPID manquantes'];
}
// Test de format base64url
if (!preg_match('/^[A-Za-z0-9_-]{87}$/', $public_key)) {
return ['status' => 'error', 'message' => 'Format clé publique invalide'];
}
return ['status' => 'success', 'message' => 'Clés VAPID valides'];
}
Service Worker non enregistré : Le problème classique ! Vérifiez que le fichier sw.js est accessible et que l’enregistrement se fait correctement. J’ajoute toujours cette vérification JS :
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered successfully');
})
.catch(error => {
console.error('SW registration failed:', error);
// Afficher un message d'erreur à l'utilisateur
});
}
Erreurs 410 (subscription expirée) : C’est normal, les subscriptions peuvent expirer. Il faut nettoyer la base :
function clean_expired_subscriptions() {
global $wpdb;
// Supprimer les subscriptions qui retournent 410
$wpdb->delete(
$wpdb->prefix . 'push_subscriptions',
['status' => 'expired']
);
}
// Programmé quotidiennement
if (!wp_next_scheduled('clean_push_subscriptions')) {
wp_schedule_event(time(), 'daily', 'clean_push_subscriptions');
}
add_action('clean_push_subscriptions', 'clean_expired_subscriptions');
Notifications bloquées par le navigateur : Pas grand-chose à faire côté serveur, mais on peut détecter le statut :
if (Notification.permission === 'denied') {
// Expliquer à l'utilisateur comment réactiver
showNotificationBlockedMessage();
}
Pour finir, j’ai mis tout le code source sur GitHub : wordpress-native-push-notifications. La documentation d’installation est complète, avec un guide pas-à-pas pour éviter les pièges courants.
Et croyez-moi, après avoir implémenté ça plusieurs fois, ces outils de debug vous feront gagner des heures !
