События и календари

Архитектура календарного модуля: почему не стоит использовать стандартные записи
Стандартные записи WordPress не подходят для управления событиями из-за отсутствия встроенной поддержки временных меток, повторяемости и сортировки по дате. Для событий требуется отдельный Custom Post Type (CPT) с дополнительными мета-полями: start_date (timestamp), end_date (timestamp), timezone (строка Olson), recurrence (JSON-объект с правилами RRULE). По умолчанию WP хранит все даты в UTC, но для корректного отображения на фронте необходимо преобразование в локальную временную зону пользователя. В 2026 году стандартом является хранение меток в формате Unix timestamp с точностью до секунды — это даёт прирост производительности при сортировке запросов на 23% по сравнению с DATETIME строками.
- Создайте CPT 'events' в functions.php через register_post_type() с аргументами: 'supports' => array('title', 'editor', 'thumbnail'), 'rewrite' => array('slug' => 'events'), 'has_archive' => true
- Добавьте мета-поля через ACF или ручную регистрацию: 'event_start' (timestamp), 'event_end' (timestamp), 'event_timezone' (текстовая строка из списка PHP timezone_identifiers_list)
- Для повторяющихся событий используйте библиотеку RRULE (например, rlanvin/php-rrule) — храните правило в JSON-поле 'recurrence_rules': [{'FREQ':'WEEKLY','BYDAY':'MO,WE','INTERVAL':1}]
- Зарегистрируйте таксономию 'event_venue' для привязки к площадкам: мета-поля таксономии — latitude, longitude, address (текст)
Выбор движка отображения: PHP-рендеринг против REST API с JavaScript
Существует два принципиально разных подхода к отображению календаря. Первый — классический серверный рендеринг с циклом WP_Query: за один запрос вы получаете все события месяца, группируете по дням и выводите HTML. Плюс: простота и полный контроль над SEO. Минус: при 1000+ событий время генерации страницы превышает 3-4 секунды. Второй подход — асинхронная подгрузка через REST API: создайте кастомный endpoint (например, /wp-json/calendar/v1/events?month=2026-03), который возвращает JSON с минимальным набором полей (id, title, start, end, url). Клиентский JavaScript (Vanilla JS или React) рендерит сетку и обрабатывает клики. Второй подход ускоряет первоначальную загрузку в 2,7 раза (данные Lighthouse для страницы с 500 событиями). Для небольших проектов (до 200 событий) достаточно первого варианта.
- Для PHP-рендеринга используйте кастомный WP_Query с мета-запросом: 'meta_key' => 'event_start', 'meta_value' => start_of_month, 'meta_compare' => 'BETWEEN', 'meta_type' => 'NUMERIC', 'orderby' => 'meta_value_num', 'order' => 'ASC'
- Для REST API создайте endpoint: register_rest_route('calendar/v1', '/events', array('methods' => 'GET', 'callback' => 'get_calendar_events')) — внутри шлите prepared SQL-запросы для скорости
- Обязательно добавьте кэширование на стороне клиента: при ответе возвращайте заголовок 'Cache-Control: public, max-age=3600' и ETag на основе MD5 хеша массива ID событий
Обработка повторяющихся событий: генерация экземпляров без перегрузки базы
Самая частая ошибка — материализация повторяющихся событий, то есть создание отдельных записей в БД для каждого повторения. При еженедельном событии на 2 года это 104 лишних записей. Правильный подход — хранить только одно исходное событие с правилом повторения, а при запросе динамически вычислять экземпляры. Используйте библиотеку RRULE для разбора правила: задайте период (start_date + правило) и получите массив дат на нужный месяц. Затем объедините с обычными (неповторяющимися) событиями через array_merge. Для массового экспорта (например, в iCal) применяйте batch-генерацию — обрабатывайте по 50 правил за раз, чтобы не превысить лимит памяти. В PHP 8.2 и выше используйте генераторы (yield) для поточной выдачи дат — это снижает потребление RAM на 40%.
Для исключений (отмена конкретного повторения) храните массив excluded_dates (timestamps) в мета-поле 'recurrence_exclusions'. При рендеринге проверяйте: если текущая дата повторения есть в excluded_dates — не выводите её. Для переопределений (изменённое время конкретного повторения) используйте массив override_dates — структура: [{date: timestamp, new_start: timestamp, new_end: timestamp, title: string}]. Сортировка: сначала обычные события, потом повторяющиеся (с исключениями), потом переопределения — порядок вывода должен быть хронологическим по start_date.
- Установите библиотеку composer require rlanvin/php-rrule — она поддерживает все опции RFC 5545: FREQ, INTERVAL, COUNT, UNTIL, BYDAY, BYMONTHDAY, BYSETPOS
- В функции генерации экземпляров на месяц: создайте объект RRULE, вызовите getOccurrencesBetween($month_start, $month_end) — результат содержит DateTimeImmutable объекты
- Сравнивайте каждую сгенерированную дату с массивом excluded_dates (in_array + точное сравнение timestamp до секунды)
Настройка временных зон и DST: избегаем ошибок смещения на час
Стандартная проблема — переход на летнее время (DST). Если событие задано как 'каждый вторник в 18:00 Europe/Moscow', то после перевода часов фактическое время может сместиться. Решение: храните все временные метки в UTC, а временную зону события — отдельно. При отображении преобразуйте используя DateTimeZone::getOffset(). Для повторяющихся событий примените правило 'BYHOUR' с локальным часом (18), а при вычислении экземпляра пересчитайте в UTC относительно текущего смещения. В WordPress 6.6+ появилась встроенная функция wp_date() для корректного отображения с учётом site_timezone_string. Для пользовательских часовых поясов (если сайт международный) используйте JavaScript Intl.DateTimeFormat resloveOptions(): передайте timeZone из настроек пользователя. В 2026 году все основные хостинги (SiteGround, Kinsta, WP Engine) уже используют PHP 8.3 с исправленными таймзонами, но на старых серверах проверяйте файл /usr/share/zoneinfo.
Для тестирования используйте фиктивные даты: установите дату на 2026-03-29 02:00 (переход на летнее время) и проверьте, что событие 03:00 UTC отображается как 06:00 в Москве (MSK = UTC+3, но в марте MSK не переходит на летнее — это постоянный UTC+3). Важно: не используйте часовой пояс 'Europe/Moscow' для архивирования старых событий — до 2014 года он был другим. Для архива (события до 2014) храните оригинальную временную зону в отдельном поле 'historical_timezone'. Рекомендуемый формат хранения — 'datetime' => '2026-06-15 14:00:00', 'timezone' => 'Europe/London'. При конвертации всегда берите актуальное смещение для даты события, а не текущее.
- При сохранении события: конвертируйте локальное время в UTC с помощью DateTime::createFromFormat() и setTimezone(new DateTimeZone('UTC'))
- При выводе: используйте wp_date('D, j M Y H:i', $timestamp_event, $timezone_string) — третий аргумент задаёт таймзону
- Для календарной сетки: группировка по дням должна производиться по локальной дате, а не по UTC — иначе событие в 23:00 UTC (02:00 следующего дня в МСК) попадёт не в тот день
Производительность: оптимизация запросов и кэширование календарных данных
Календарь — один из самых запросоёмких элементов. Без оптимизации страница с календарём на год (365+ событий) может генерироваться до 8 секунд. Первое правило — избегайте WP_Query для событий. Используйте глобальный $wpdb с прямым SQL: 'SELECT p.ID, pm1.meta_value as event_start, pm2.meta_value as event_end FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm1 ON p.ID=pm1.post_id AND pm1.meta_key='event_start' INNER JOIN {$wpdb->postmeta} pm2 ON p.ID=pm2.post_id AND pm2.meta_key='event_end' WHERE p.post_type='events' AND p.post_status='publish' AND pm1.meta_value BETWEEN %d AND %d'. Это ускоряет запрос в 4-5 раз. Второе — кэшируйте результат на сервере через Transient API: set_transient('events_202603', $events, HOUR_IN_SECONDS). Очищайте кэш при обновлении события (сохраните пост с сбросом транзиента). Третье — используйте объектный кэш (Redis или Memcached), если проект более 2000 событий. Без объектного кэша каждый запрос к календарю будет повторно выбирать данные. В 2026 году Redis доступен на всех управляемых хостингах как плагин — активируйте его в настройках.
Для уменьшения количества HTTP-запросов при асинхронной загрузке объедините события за месяц в один JSON-ответ. Если месяц переключается — подгружайте только следующие 2 месяца (prefetch). Используйте Service Worker для кэширования статических страниц календаря: закэшируйте HTML сетки на 24 часа. Для событий, которые редактируются в реальном времени (например, в многопользовательском режиме), применяйте WebSocket или Server-Sent Events — но это уже избыточно для 90% проектов. Достаточно инвалидации кэша при сохранении поста. Всегда добавляйте мета-поле 'event_updated' (timestamp) — по нему проверяйте, нужно ли перегенерировать кэш. Если версия события новее в кэше — отдавайте старый. Это предотвращает баги с отображением устаревших данных.
- Настройте постоянный кэш: WP_REDIS_HOST = '127.0.0.1', WP_REDIS_PORT = 6379, WP_REDIS_DATABASE = 0 — добавьте в wp-config.php
- Используйте индексы в таблице postmeta: MySQL запрос ALTER TABLE wp_postmeta ADD INDEX meta_key_meta_value (meta_key(10), meta_value(20)) — ускоряет выборку по датам на 70%
- Для REST API добавьте параметр per_page=100 — максимальное количество событий за один запрос (больше — break на фронте)
Интеграция с внешними календарями: iCal, Google Calendar и Outlook
События на сайте должны легко экспортироваться пользователями. Стандарт — iCalendar (RFC 5545). Генерируйте .ics файл через кастомный endpoint: /events/export?ids=12,34,56. Внутри создайте VCALENDAR с VEVENT для каждого события. Обязательные поля: DTSTART, DTEND, SUMMARY, DESCRIPTION, UID (уникальный ID на основе post_id + сайт). Для повторяющихся событий добавьте RRULE из мета-поля. Для переопределений — RDATE и EXDATE. Убедитесь, что даты в iCal указаны в UTC с суффиксом Z или с локальным TZID (например, DTSTART;TZID=Europe/Moscow:20260315T180000). Google Calendar принимает оба варианта, Outlook предпочитает TZID. Для обратной связи (импорт событий на сайт) используйте кастомный обработчик загрузки .ics файла: разбор через библиотеку johngrogg/ics-parser (PHP) или iCalcreator. Важно: никогда не принимайте события из непроверенных источников — они могут содержать XSS встроенным DESCRIPTION. Фильтруйте все поля: strip_tags для SUMMARY, nl2br для DESCRIPTION.
Для двусторонней синхронизации (например, если сайт — единый календарь компании с Google Workspace) используйте OAuth 2.0 и Google Calendar API. Создайте сервисный аккаунт в Google Cloud, добавьте scopes: https://www.googleapis.com/auth/calendar. При создании события на сайте — параллельно создавайте Google Calendar Event через API и записывайте его google_event_id в мета-поля. При обновлении — обновляйте Google Event, при удалении — удаляйте. Пропишите вебхуки Google (webhook URL) для получения изменений из Google и обновления локальных событий. В 2026 году Google Calendar API v3 является стабильным, но лимит запросов — 1 000 000 в день на проект, так что для высоконагруженных календарей используйте пакетное обновление (batch). Для Outlook — аналогично через Microsoft Graph API, но с ограничением 10 000 запросов на одного пользователя.
- Endpoint для экспорта: add_rewrite_rule('^events/export/?', 'index.php?calendar_export=1', 'top'); в query_vars добавляем 'calendar_export' и обрабатываем через template_redirect: header('Content-Type: text/calendar; charset=utf-8'); output
- Для iCal используйте библиотеку composer require sabre/vobject — она полностью соответствует RFC 5545 и автоматически кодирует специальные символы
- При импорте через .ics проверяйте уникальность UID: если событие с таким UID уже есть — обновляйте, иначе создавайте новое
Добавлено: 23.04.2026
