Introduction
Les shortcodes WordPress sont des marqueurs textuels puissants qui permettent d’insérer du contenu dynamique et des fonctionnalités complexes directement dans vos articles, pages ou widgets. Introduits dans WordPress 2.5, ils constituent un pont élégant entre le contenu statique et les fonctionnalités PHP avancées.
Dans ce guide complet, nous explorerons l’architecture des shortcodes, leur création, leur sécurisation et leur optimisation pour des applications professionnelles.
Table des matières
- Comprendre l’architecture des shortcodes
- Créer un shortcode simple
- Shortcodes avec attributs
- Shortcodes avec contenu encapsulé
- Shortcodes imbriqués et auto-fermants
- Sécurité et validation des données
- Optimisation des performances
- Cas d’usage avancés
- Debugging et résolution de problèmes
- Bonnes pratiques professionnelles
1. Comprendre l’architecture des shortcodes
Le cycle de vie d’un shortcode
Lorsque WordPress génère une page, il passe par plusieurs phases de traitement du contenu :
Récupération du contenu depuis la BDD
↓
Application des filtres 'the_content'
↓
Parsing et détection des shortcodes
↓
Exécution des callbacks associés
↓
Remplacement des balises par le contenu généré
↓
Affichage final
Structure interne
WordPress utilise une expression régulière pour identifier les shortcodes dans le contenu :
// Pattern simplifié utilisé par WordPress
$pattern = '/[(w+)([^]]*)](?:(.*?)[/1])?/s';
Les shortcodes sont stockés dans un tableau global $shortcode_tags qui associe chaque nom de shortcode à sa fonction callback.
2. Créer un shortcode simple
Exemple basique : Affichage de la date du jour
%s',
date('c'), // Format ISO 8601
esc_html($current_date)
);
}
add_shortcode('date_actuelle', 'yawc_date_shortcode');
Utilisation :
[date_actuelle]
Points importants :
echoesc_html(), esc_attr(), etc.date_i18n() pour la localisationShortcode avec buffer output
Si vous devez intégrer du code complexe avec des templates :
3. Shortcodes avec attributs
Gestion avancée des attributs
5,
'categorie' => '',
'ordre' => 'DESC',
'orderby' => 'date',
'exclu' => '', // IDs à exclure
'afficher' => 'titre,extrait,image', // Éléments à afficher
'classe' => '',
),
$atts,
'articles_recents' // Tag du shortcode pour le filtrage
);
// Valider et nettoyer les entrées
$nombre = absint($atts['nombre']);
$nombre = min($nombre, 50); // Limiter à 50 pour les performances
$ordre = in_array(strtoupper($atts['ordre']), array('ASC', 'DESC'))
? strtoupper($atts['ordre'])
: 'DESC';
// Liste blanche des orderby autorisés
$orderby_allowed = array('date', 'title', 'rand', 'comment_count', 'modified');
$orderby = in_array($atts['orderby'], $orderby_allowed)
? $atts['orderby']
: 'date';
// Construire les arguments de requête
$args = array(
'posts_per_page' => $nombre,
'order' => $ordre,
'orderby' => $orderby,
'ignore_sticky_posts' => true,
'no_found_rows' => true, // Performance : pas besoin de pagination
);
// Ajouter le filtre de catégorie si spécifié
if (!empty($atts['categorie'])) {
$args['category_name'] = sanitize_text_field($atts['categorie']);
}
// Gérer les exclusions
if (!empty($atts['exclu'])) {
$exclu_ids = array_map('absint', explode(',', $atts['exclu']));
$args['post__not_in'] = $exclu_ids;
}
// Exécuter la requête
$query = new WP_Query($args);
if (!$query->have_posts()) {
return 'Aucun article trouvé.
';
}
// Déterminer quels éléments afficher
$afficher = array_map('trim', explode(',', $atts['afficher']));
// Construire le HTML
$classe_container = 'yawc-recent-posts';
if (!empty($atts['classe'])) {
$classe_container .= ' ' . sanitize_html_class($atts['classe']);
}
ob_start();
?>
have_posts()) : $query->the_post(); ?>
Exemples d'utilisation :
[articles_recents nombre="3"]
[articles_recents nombre="5" categorie="tutoriels" ordre="ASC"]
[articles_recents nombre="4" afficher="titre,image,date" classe="grid-layout"]
[articles_recents orderby="rand" nombre="3" exclu="15,28,42"]
Filtrage des attributs
Permettre aux développeurs de modifier les attributs :
Utilisation du filtre :
4. Shortcodes avec contenu encapsulé
Box de mise en évidence
'info', // info, warning, success, error
'titre' => '',
'icone' => 'true',
'fermable' => 'false',
), $atts, 'box');
// Validation du type
$types_valides = array('info', 'warning', 'success', 'error');
$type = in_array($atts['type'], $types_valides) ? $atts['type'] : 'info';
// Icônes par type
$icones = array(
'info' => '',
'warning' => '',
'success' => '',
'error' => '',
);
// Traiter le contenu (important pour les shortcodes imbriqués)
$content = do_shortcode($content);
// Construire le HTML
$html = sprintf(
'',
esc_attr($type)
);
if ($atts['fermable'] === 'true') {
$html .= '';
}
if (!empty($atts['titre']) || $atts['icone'] === 'true') {
$html .= '';
if ($atts['icone'] === 'true') {
$html .= $icones[$type];
}
if (!empty($atts['titre'])) {
$html .= sprintf(
'%s
',
esc_html($atts['titre'])
);
}
$html .= '';
}
$html .= sprintf(
'%s',
wpautop($content) // Ajouter les paragraphes automatiquement
);
$html .= '';
return $html;
}
add_shortcode('box', 'yawc_box_shortcode');
Utilisation :
[box type="warning" titre="Attention"]
Cette fonctionnalité est en version bêta.
[/box]
[box type="success" icone="true" fermable="true"]
Votre inscription a été enregistrée avec succès !
[/box]
CSS associé
/* yawc-shortcodes.css */
.yawc-box {
position: relative;
padding: 1.5rem;
margin: 1.5rem 0;
border-radius: 8px;
border-left: 4px solid;
}
.yawc-box-info {
background-color: #e3f2fd;
border-left-color: #2196f3;
color: #0d47a1;
}
.yawc-box-warning {
background-color: #fff3e0;
border-left-color: #ff9800;
color: #e65100;
}
.yawc-box-success {
background-color: #e8f5e9;
border-left-color: #4caf50;
color: #1b5e20;
}
.yawc-box-error {
background-color: #ffebee;
border-left-color: #f44336;
color: #b71c1c;
}
.yawc-box-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.yawc-box-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
.yawc-icon {
width: 24px;
height: 24px;
fill: currentColor;
}
.yawc-box-close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
}
.yawc-box-close:hover {
opacity: 1;
}
.yawc-box-content p:last-child {
margin-bottom: 0;
}
JavaScript pour les boîtes fermables
// yawc-shortcodes.js
document.addEventListener('DOMContentLoaded', function() {
const closeButtons = document.querySelectorAll('.yawc-box-close');
closeButtons.forEach(button => {
button.addEventListener('click', function() {
const box = this.closest('.yawc-box');
box.style.transition = 'opacity 0.3s, transform 0.3s';
box.style.opacity = '0';
box.style.transform = 'translateY(-10px)';
setTimeout(() => {
box.remove();
}, 300);
});
});
});
5. Shortcodes imbriqués et auto-fermants
Système de tabs avec imbrication
'default', // default, pills, underline
), $atts, 'tabs');
// Reset le compteur d'onglets pour chaque groupe
global $yawc_tab_index;
$yawc_tab_index = 0;
// Stocker l'ID du groupe pour les onglets enfants
global $yawc_current_tab_group;
$yawc_current_tab_group = $tab_group_id;
// Parser le contenu pour extraire les titres des onglets
$content = do_shortcode($content);
return sprintf(
'%s',
esc_attr($atts['style']),
$tab_group_id,
$content
);
}
add_shortcode('tabs', 'yawc_tabs_shortcode');
/**
* Shortcode pour un onglet individuel
*/
function yawc_tab_shortcode($atts, $content = null) {
global $yawc_tab_index, $yawc_current_tab_group;
$atts = shortcode_atts(array(
'titre' => 'Onglet ' . ($yawc_tab_index + 1),
'icone' => '',
'actif' => 'false',
), $atts, 'tab');
$is_active = ($yawc_tab_index === 0 || $atts['actif'] === 'true');
$yawc_tab_index++;
$tab_id = sprintf('tab-%d-%d', $yawc_current_tab_group, $yawc_tab_index);
$panel_id = sprintf('panel-%d-%d', $yawc_current_tab_group, $yawc_tab_index);
// Construire le bouton d'onglet (sera repositionné par JS)
$tab_button = sprintf(
'',
$is_active ? ' active' : '',
$is_active ? 'true' : 'false',
esc_attr($panel_id),
esc_attr($tab_id),
$yawc_tab_index,
!empty($atts['icone']) ? '' : '',
esc_html($atts['titre'])
);
// Construire le panneau de contenu
$tab_panel = sprintf(
'%s',
$is_active ? ' active' : '',
esc_attr($tab_id),
esc_attr($panel_id),
$yawc_tab_index,
do_shortcode($content)
);
// Retourner le bouton + panneau (le JS réorganisera)
return sprintf(
'%s',
esc_attr($tab_button),
$tab_panel
);
}
add_shortcode('tab', 'yawc_tab_shortcode');
JavaScript pour les tabs
// yawc-tabs.js
class YAWCTabs {
constructor(container) {
this.container = container;
this.tabGroup = container.dataset.tabGroup;
this.init();
}
init() {
// Créer la liste des onglets
const tabList = document.createElement('div');
tabList.className = 'yawc-tab-list';
tabList.setAttribute('role', 'tablist');
// Extraire tous les boutons et les ajouter à la liste
const wrappers = this.container.querySelectorAll('.yawc-tab-wrapper');
const panels = [];
wrappers.forEach(wrapper => {
const buttonHTML = wrapper.dataset.tabButton;
const panel = wrapper.querySelector('.yawc-tab-panel');
// Créer le bouton depuis le HTML stocké
const temp = document.createElement('div');
temp.innerHTML = buttonHTML;
const button = temp.firstChild;
tabList.appendChild(button);
panels.push(panel);
// Ajouter l'écouteur d'événements
button.addEventListener('click', () => this.switchTab(button, panel));
});
// Réorganiser le DOM
this.container.innerHTML = '';
this.container.appendChild(tabList);
const panelsContainer = document.createElement('div');
panelsContainer.className = 'yawc-tab-panels';
panels.forEach(panel => panelsContainer.appendChild(panel));
this.container.appendChild(panelsContainer);
// Ajouter la navigation au clavier
this.addKeyboardNavigation(tabList);
}
switchTab(button, panel) {
// Désactiver tous les onglets
this.container.querySelectorAll('.yawc-tab-button').forEach(btn => {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
});
this.container.querySelectorAll('.yawc-tab-panel').forEach(pnl => {
pnl.classList.remove('active');
});
// Activer l'onglet sélectionné
button.classList.add('active');
button.setAttribute('aria-selected', 'true');
panel.classList.add('active');
// Focus sur le panneau pour l'accessibilité
panel.focus();
}
addKeyboardNavigation(tabList) {
const buttons = tabList.querySelectorAll('.yawc-tab-button');
buttons.forEach((button, index) => {
button.addEventListener('keydown', (e) => {
let targetIndex = index;
if (e.key === 'ArrowRight') {
targetIndex = (index + 1) % buttons.length;
} else if (e.key === 'ArrowLeft') {
targetIndex = (index - 1 + buttons.length) % buttons.length;
} else if (e.key === 'Home') {
targetIndex = 0;
} else if (e.key === 'End') {
targetIndex = buttons.length - 1;
} else {
return;
}
e.preventDefault();
buttons[targetIndex].click();
buttons[targetIndex].focus();
});
});
}
}
// Initialiser tous les groupes d'onglets
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.yawc-tabs').forEach(container => {
new YAWCTabs(container);
});
});
Utilisation :
[tabs style="pills"]
[tab titre="Introduction" actif="true"]
Contenu de l'introduction avec du __Markdown__ si vous utilisez un parser.
[/tab]
[tab titre="Configuration"]
Instructions de configuration détaillées.
[/tab]
[tab titre="Exemples de code"]
php
echo "Hello World";
[/tab]
[/tabs]
6. Sécurité et validation des données
Principes essentiels
Shortcode sécurisé avec formulaire
get_option('admin_email'),
'objet' => 'Nouveau message depuis le site',
'redirect' => '',
), $atts, 'contact_form');
// Valider l'email
$email_destination = is_email($atts['email']) ? $atts['email'] : get_option('admin_email');
// Générer un ID unique pour ce formulaire
$form_id = 'yawc-contact-' . wp_generate_password(8, false);
// Traitement du formulaire si soumis
$message_sent = false;
$error_message = '';
if (isset($*POST['yawc_contact_submit']) &&
isset($*POST['yawc_form_id']) &&
$*POST['yawc_form_id'] === $form_id) {
// Vérifier le nonce
if (!isset($*POST['yawc_contact_nonce']) ||
!wp_verify_nonce($*POST['yawc_contact_nonce'], 'yawc_contact_action')) {
$error_message = 'Erreur de sécurité. Veuillez réessayer.';
} else {
// Valider et nettoyer les données
$nom = isset($*POST['nom']) ? sanitize_text_field($*POST['nom']) : '';
$email = isset($*POST['email']) ? sanitize_email($*POST['email']) : '';
$sujet = isset($*POST['sujet']) ? sanitize_text_field($*POST['sujet']) : '';
$message = isset($*POST['message']) ? sanitize_textarea_field($*POST['message']) : '';
// Validation
$errors = array();
if (empty($nom)) {
$errors[] = 'Le nom est requis.';
}
if (empty($email) || !is_email($email)) {
$errors[] = 'Une adresse email valide est requise.';
}
if (empty($message)) {
$errors[] = 'Le message est requis.';
}
// Protection anti-spam basique
if (isset($*POST['website']) && !empty($*POST['website'])) {
// Champ honeypot rempli = spam
$errors[] = 'Détection de spam.';
}
if (empty($errors)) {
// Préparer l'email
$to = $email_destination;
$subject = $atts['objet'] . ' - ' . $sujet;
$body = sprintf(
"Nouveau message de contactnn" .
"Nom: %sn" .
"Email: %sn" .
"Sujet: %snn" .
"Message:n%snn" .
"---n" .
"Envoyé depuis: %s",
$nom,
$email,
$sujet,
$message,
home_url()
);
$headers = array(
'Reply-To: ' . $nom . ' <' . $email . '>',
'Content-Type: text/plain; charset=UTF-8'
);
// Envoyer l'email
if (wp_mail($to, $subject, $body, $headers)) {
$message_sent = true;
// Hook pour les actions post-envoi
do_action('yawc_contact_form_sent', array(
'nom' => $nom,
'email' => $email,
'sujet' => $sujet,
'message' => $message,
));
// Redirection si configurée
if (!empty($atts['redirect'])) {
wp_safe_redirect(esc_url($atts['redirect']));
exit;
}
} else {
$error_message = 'Erreur lors de l'envoi du message. Veuillez réessayer.';
}
} else {
$error_message = implode('
', $errors);
}
}
}
// Afficher le formulaire
ob_start();
?>
Protection contre les injections
'Rechercher...',
'button_text' => 'Rechercher',
'post_type' => 'post',
), $atts, 'search');
// Valider le post_type contre une liste blanche
$allowed_post_types = get_post_types(array('public' => true), 'names');
$post_types = array_map('trim', explode(',', $atts['post_type']));
$post_types = array_filter($post_types, function($pt) use ($allowed_post_types) {
return in_array($pt, $allowed_post_types, true);
});
if (empty($post_types)) {
$post_types = array('post');
}
ob_start();
?>
7. Optimisation des performances
Mise en cache des shortcodes
3600, // 1 heure par défaut
), $atts, 'stats');
// Créer une clé de cache unique basée sur les attributs
$cache_key = 'yawc_stats*' . md5(serialize($atts));
// Essayer de récupérer depuis le cache
$cached_output = get_transient($cache_key);
if ($cached_output !== false) {
return $cached_output . '';
}
// Générer le contenu (opération coûteuse)
$output = yawc_generate_stats_html();
// Mettre en cache
set_transient($cache_key, $output, absint($atts['cache_duration']));
return $output;
}
add_shortcode('stats', 'yawc_cached_stats_shortcode');
/**
* Nettoyer le cache lors de la publication d'un article
*/
add_action('save_post', function($post_id) {
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
// Supprimer tous les transients de stats
global $wpdb;
$wpdb->query(
"DELETE FROM $wpdb->options
WHERE option_name LIKE '*transient_yawc_stats*%'
OR option_name LIKE '*transient_timeout_yawc_stats*%'"
);
});
function yawc_generate_stats_html() {
global $wpdb;
$stats = array(
'posts' => wp_count_posts('post')->publish,
'pages' => wp_count_posts('page')->publish,
'comments' => wp_count_comments()->approved,
'users' => count_users()['total_users'],
);
ob_start();
?>
Articles
Pages
Commentaires
Utilisateurs
Chargement conditionnel des assets
8. Cas d'usage avancés
Shortcode avec requête Ajax
'',
'button_text' => 'Charger plus',
'container' => 'posts',
), $atts, 'ajax_loader');
if (empty($atts['action'])) {
return 'Erreur: action requise
';
}
$loader_id = 'ajax-loader-' . wp_generate_password(8, false);
// Passer les données au JavaScript
wp_localize_script('yawc-ajax-loader', 'yawcAjaxLoader', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('yawc_ajax_loader'),
));
ob_start();
?>
'post',
'posts_per_page' => 6,
'paged' => $page,
'post_status' => 'publish',
);
$query = new WP_Query($args);
if (!$query->have_posts()) {
wp_send_json_error(array('message' => 'Aucun article supplémentaire'));
}
ob_start();
while ($query->have_posts()) {
$query->the_post();
?>
###
$html,
'has_more' => $page < $query->max_num_pages,
'next_page' => $page + 1,
));
}
add_action('wp_ajax_load_more_posts', 'yawc_load_more_posts_ajax');
add_action('wp_ajax_nopriv_load_more_posts', 'yawc_load_more_posts_ajax');
JavaScript correspondant:
// yawc-ajax-loader.js
(function($) {
'use strict';
class AjaxLoader {
constructor(element) {
this.element = $(element);
this.action = this.element.data('action');
this.button = this.element.find('.yawc-load-more');
this.container = this.element.find('.yawc-ajax-content');
this.spinner = this.element.find('.yawc-ajax-spinner');
this.page = 1;
this.button.on('click', () => this.loadMore());
}
loadMore() {
this.page++;
this.button.prop('disabled', true);
this.spinner.show();
$.ajax({
url: yawcAjaxLoader.ajaxurl,
type: 'POST',
data: {
action: this.action,
nonce: yawcAjaxLoader.nonce,
page: this.page
},
success: (response) => {
if (response.success) {
this.container.append(response.data.html);
if (!response.data.has_more) {
this.button.text('Aucun article supplémentaire').prop('disabled', true);
}
} else {
alert(response.data.message);
this.button.hide();
}
},
error: () => {
alert('Erreur lors du chargement');
},
complete: () => {
this.spinner.hide();
this.button.prop('disabled', false);
}
});
}
}
$(document).ready(function() {
$('.yawc-ajax-loader').each(function() {
new AjaxLoader(this);
});
});
})(jQuery);
Shortcode avec géolocalisation
'',
'ville' => '',
'fallback' => 'true',
), $atts, 'geo_content');
// Détecter le pays de l'utilisateur
$user_country = yawc_get_user_country();
$user_city = yawc_get_user_city();
$show_content = false;
if (!empty($atts['pays'])) {
$allowed_countries = array_map('trim', explode(',', $atts['pays']));
$show_content = in_array($user_country, $allowed_countries, true);
}
if (!empty($atts['ville']) && $show_content) {
$allowed_cities = array_map('trim', explode(',', $atts['ville']));
$show_content = in_array($user_city, $allowed_cities, true);
}
if ($show_content || $atts['fallback'] === 'true') {
return do_shortcode($content);
}
return '';
}
add_shortcode('geo_content', 'yawc_geo_content_shortcode');
/**
* Obtenir le pays de l'utilisateur via API ou cache
*/
function yawc_get_user_country() {
// Vérifier le cache de session
if (isset($*SESSION['yawc_user_country'])) {
return $*SESSION['yawc_user_country'];
}
$ip = yawc_get_user_ip();
// Utiliser une API de géolocalisation
$response = wp_remote_get('http://ip-api.com/json/' . $ip);
if (is_wp_error($response)) {
return 'UNKNOWN';
}
$data = json_decode(wp_remote_retrieve_body($response), true);
$country = isset($data['countryCode']) ? $data['countryCode'] : 'UNKNOWN';
// Mettre en cache
$*SESSION['yawc_user_country'] = $country;
return $country;
}
function yawc_get_user_ip() {
$ip = '';
if (!empty($*SERVER['HTTP_CLIENT_IP'])) {
$ip = $*SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($*SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $*SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $*SERVER['REMOTE_ADDR'];
}
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
}
9. Debugging et résolution de problèmes
Mode debug pour shortcodes
$tag, 'attributes' => $attr, 'output_length' => strlen($output), 'time' => microtime(true) - $start_time, 'memory' => memory_get_usage(), ); return $output; } public static function display_logs() { if (empty(self::$logs)) { return; } echo ''; echo 'Shortcode Debug Info
'; echo ''; echo '
'; foreach (self::$logs as $log) { printf( ' Tag Attributes Output Length Time (ms) Memory (MB) %s %s%d %.2f %.2f ',
esc_html($log['tag']),
esc_html(print_r($log['attributes'], true)),
$log['output_length'],
$log['time'] 1000,
$log['memory'] / 1024 / 1024
);
}echo '
';
}
}YAWC_Shortcode_Debug::init();
### Vérification des shortcodes non enregistrés
'; echo '**Shortcodes non enregistrés détectés:** '; echo implode(', ', array_map('esc_html', $unregistered)); echo '