Lorsque l’on gère un agenda sous WordPress, on se retrouve vite avec un besoin très concret : proposer un export ICS propre, ciblé et exploitable par une application tierce, sans exposer l’ensemble des événements du site. C’est exactement le cas ici avec ce code qui génère un flux calendrier personnalisé basé sur certaines catégories d’événements, tout en excluant automatiquement les contenus déjà terminés.

L’intérêt de cette approche, c’est qu’elle reste légère, totalement intégrée à WordPress et ne dépend pas d’un plugin supplémentaire. Vous créez votre propre endpoint .ics, vous choisissez précisément les taxonomies à inclure, et vous gardez la main sur chaque champ du calendrier : titre, description, lieu, image, URL et fuseau horaire. Le résultat est particulièrement pratique pour connecter un agenda WordPress à une application mobile, un service tiers comme GoodBarber ou un outil métier qui consomme des fichiers ICS.

Dans cet article, je vais vous montrer comment mettre en place ce flux ICS WordPress sur mesure, comment le sécuriser avec des fallbacks sur les dates, et pourquoi cette méthode est souvent plus fiable qu’un export générique lorsqu’on doit répondre à un besoin précis côté diffusion d’événements.

Pourquoi créer un flux ICS WordPress filtré par catégories


Sur un site qui publie beaucoup d’événements, un export ICS global devient vite difficile à exploiter. Une application mobile, un agenda externe ou un partenaire n’a pas toujours besoin de l’intégralité de vos contenus. Souvent, seules certaines catégories doivent remonter : culture, sport, vie locale, événements institutionnels, ou encore un sous-ensemble destiné à une app spécifique.

C’est là que ce type de développement prend tout son sens. Vous créez une URL dédiée, ici agenda-categories.ics, qui retourne uniquement les événements appartenant aux catégories souhaitées. Cela permet de diffuser un calendrier propre, plus léger et mieux adapté au besoin réel.

L’autre intérêt, c’est la maîtrise complète du format de sortie. Vous choisissez les champs envoyés dans chaque VEVENT, la manière de gérer les dates, les images, le lieu, ou encore les URL de redirection vers la fiche événement.

Le fonctionnement du flux personnalisé


Le principe repose sur trois étapes simples :

  • créer une route WordPress dédiée au fichier .ics
  • récupérer uniquement les événements concernés
  • générer manuellement le format VCALENDAR

La première partie ajoute une règle de réécriture pour exposer une URL lisible :

add_action('init', function () {
add_rewrite_rule('^agenda-categories\.ics$', 'index.php?custom_events_ics=1', 'top');
});

Une fois cette URL détectée, WordPress bascule sur la génération du flux au lieu d’afficher un template classique.

La requête récupère ensuite les événements du type event, triés par date de début, avec un filtre sur les catégories via tax_query. Le point intéressant ici, c’est le filtrage des événements terminés grâce à _event_end_local, ce qui évite d’alourdir le calendrier avec du contenu passé.

Le code complet à intégrer


Voici la solution complète telle quelle, à intégrer dans le functions.php du thème enfant ou dans un plugin sur mesure.

// NOUVEAU ICS (pour goodbarber)
/**
* Flux ICS personnalisé : uniquement les événements des catégories demandées
*/
add_action('init', function () {
add_rewrite_rule('^agenda-categories\.ics$', 'index.php?custom_events_ics=1', 'top');
});
add_filter('query_vars', function ($vars) {
$vars[] = 'custom_events_ics';
return $vars;
});
add_action('template_redirect', function () {
if ((int) get_query_var('custom_events_ics') !== 1) {
return;
}
$term_ids = array(1,2,3,4);
$timezone = wp_timezone();
$events = get_posts(array(
'post_type' => 'event',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'meta_value',
'meta_key' => '_event_start_local',
'order' => 'ASC',
'tax_query' => array(
array(
'taxonomy' => 'event-categories',
'field' => 'term_id',
'terms' => $term_ids,
'operator' => 'IN',
),
),
'meta_query' => array(
array(
'key' => '_event_end_local',
'value' => current_time('mysql'),
'compare' => '>=',
'type' => 'DATETIME',
),
),
));
nocache_headers();
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: attachment; filename="agenda-categories-348-452.ics"');
echo "BEGIN:VCALENDAR\r\n";
echo "VERSION:2.0\r\n";
echo "PRODID:-//Chateauroux Metropole//Agenda categories 348 452//FR\r\n";
echo "CALSCALE:GREGORIAN\r\n";
echo "METHOD:PUBLISH\r\n";
echo "X-WR-CALNAME:Agenda catégories 348 et 452\r\n";
echo "X-WR-TIMEZONE:" . esc_ics($timezone->getName()) . "\r\n";
foreach ($events as $event) {
$start_raw = get_post_meta($event->ID, '_event_start_local', true);
$end_raw = get_post_meta($event->ID, '_event_end_local', true);
$image_id = get_post_thumbnail_id($event->ID);
$image_url = $image_id ? wp_get_attachment_url($image_id) : '';

// Fallback si _event_start_local / _event_end_local sont absents
if (empty($start_raw)) {
$start_date = get_post_meta($event->ID, '_event_start_date', true);
$start_time = get_post_meta($event->ID, '_event_start_time', true);
if ($start_date) {
$start_raw = trim($start_date . ' ' . ($start_time ?: '00:00:00'));
}
}

if (empty($end_raw)) {
$end_date = get_post_meta($event->ID, '_event_end_date', true);
$end_time = get_post_meta($event->ID, '_event_end_time', true);
if ($end_date) {
$end_raw = trim($end_date . ' ' . ($end_time ?: '23:59:59'));
}
}
if (empty($start_raw)) {
continue;
}

try {
$start = new DateTime($start_raw, $timezone);
$end = !empty($end_raw) ? new DateTime($end_raw, $timezone) : clone $start;
// Sécurité : si fin <= début, on ajoute 1 heure
if ($end <= $start) {
$end = clone $start;
$end->modify('+1 hour');
}
} catch (Exception $e) {
continue;
}
$title = get_the_title($event->ID);
$description = wp_strip_all_tags(get_post_field('post_content', $event->ID));
$permalink = get_permalink($event->ID);
$location = '';
$location_name = get_post_meta($event->ID, '_location_name', true);

if (!empty($location_name)) {
$location = $location_name;
}

$uid = 'event-' . $event->ID . '@' . parse_url(home_url(), PHP_URL_HOST);
$dtstamp = gmdate('Ymd\THis\Z');

echo "BEGIN:VEVENT\r\n";
echo "UID:" . esc_ics($uid) . "\r\n";
echo "DTSTAMP:" . $dtstamp . "\r\n";
echo "DTSTART:" . $start->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z') . "\r\n";
echo "DTEND:" . $end->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z') . "\r\n";
echo "SUMMARY:" . esc_ics($title) . "\r\n";

if (!empty($description)) {
echo "DESCRIPTION:" . esc_ics($description . "\n\n" . $permalink) . "\r\n";
} else {
echo "DESCRIPTION:" . esc_ics($permalink) . "\r\n";
}

if (!empty($location)) {
echo "LOCATION:" . esc_ics($location) . "\r\n";
}

if (!empty($image_url)) {
echo "ATTACH;FMTTYPE=image/jpeg:" . esc_ics($image_url) . "\r\n";
}

echo "URL:" . esc_ics($permalink) . "\r\n";
echo "STATUS:CONFIRMED\r\n";
echo "TRANSP:OPAQUE\r\n";
echo "END:VEVENT\r\n";
}
echo "END:VCALENDAR\r\n";
exit;
});

function esc_ics($string) {
$string = html_entity_decode((string) $string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$string = wp_strip_all_tags($string);
$string = str_replace("\\", "\\\\", $string);
$string = str_replace(";", "\;", $string);
$string = str_replace(",", "\,", $string);
$string = str_replace(array("\r\n", "\r", "\n"), '\n', $string);
return $string;
}

Les points techniques à retenir

Ce code va un peu plus loin qu’un simple export standard.

1) Le filtrage par taxonomie

La variable suivante pilote les catégories remontées :

$term_ids = array(1,2,3,4);

Vous adaptez simplement les IDs aux catégories à exposer.

2) L’exclusion des événements passés

Le meta_query évite d’envoyer des données obsolètes :

'compare' => '>=',

Le flux reste ainsi toujours à jour sans nettoyage manuel.

3) Les fallbacks sur les dates

Si votre source ne renseigne pas _event_start_local, le script tente de reconstruire la date à partir d’autres métadonnées. C’est une sécurité utile quand plusieurs systèmes alimentent les événements.

Variante pour The Events Calendar


Ce qui change dans le code pour l’autre module bien connu des événements (The Events Calendar) :

1) La gestion des divers champs, qui ne sont pas nommés pareil

    // Dates        
            $start_raw = get_post_meta($event->ID, '_EventStartDate', true);
            $end_raw   = get_post_meta($event->ID, '_EventEndDate', true);
    // Location
            $location = get_post_meta($event->ID, '_VenueAddress', true);
            $city     = get_post_meta($event->ID, '_VenueCity', true);

    2) La requête pour aller chercher les événements (voir en gras)

    $events = get_posts(array(
            'post_type'      => 'tribe_events',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'orderby'        => 'meta_value',
            'meta_key'       => '_EventStartDate',
            'order'          => 'ASC',
            'tax_query'      => array(
                array(
                    'taxonomy' => 'tribe_events_cat',
                    'field'    => 'term_id',
                    'terms'    => $term_ids,
                    'operator' => 'IN',
                ),
            ),
            'meta_query' => array(
                array(
                    'key'     => '_EventEndDate',
                    'value'   => $now,
                    'compare' => '>=',
                    'type'    => 'DATETIME',
                ),
            ),
        ));

    Pourquoi cette méthode fonctionne bien avec une application mobile


    Dans le cadre d’une intégration avec GoodBarber ou un autre agrégateur, ce format permet :

    • un endpoint stable
    • des événements futurs uniquement
    • une URL ICS unique à consommer
    • l’image mise en pièce jointe
    • le lien direct vers la fiche WordPress
    • une gestion propre du fuseau horaire

    Vous évitez ainsi les exports trop larges ou mal formatés que certains plugins proposent par défaut.

    Quelques conseils avant la mise en production


    Ne pas oublier !
    Pensez à vider les permaliens WordPress après ajout de la rewrite rule, sinon l’URL .ics ne répondra pas correctement.

    Je vous conseille aussi de tester le flux dans :

    • Google Calendar
    • Apple Calendar
    • Outlook
    • votre application mobile cible

    Tous les clients ICS n’interprètent pas exactement les mêmes champs, notamment ATTACH.

    Enfin, si votre base contient beaucoup d’événements, il peut être intéressant d’ajouter un système de cache transitoire pour éviter une génération complète à chaque appel du fichier.