История создания двух ссылок для добавления мероприятия в календарь
Ну казалось бы: две маленькие простые странные ссылки «Добавить в календарь» среди кучи интерактивных элементов на странице. При этом я получил ответы на вопросы типа «почему формат календарей устроен именно так?».
С чего всё началось ¶
На самом-то деле, конечно, с глупости. На каком-то «сером» сайте-помойке, типа «скачать субтитры из видео ютуба», где тонна всякой хреновой рекламы и друных схем обмана наивных посетителей. Попал я на такой сайт с айфона.
Встретил забавную штуку: через какое-то время взаимодействия с сайтом (успел вставить скопированную ссылку на видос), появилось уведомление о предложении добавить чего-то там фид-ленту в мой календарь.
Из любопытства и исследовательского взгляда — согласился. И эта херня подписала меня на ical-feed какого-то «новостного агентства», засрав мне весь календарь, блин. Создав отдельный типа событий в календаре и при этом подтягивая данные из своей ленты из внешнего источника. Забавная же хрень. Я тоже такую захотел.
Понравилось. Как идея того, что можно сделать подписку на ленту мероприятий. Как раз то, что нужно мне в проекте.
Контекст истории в проекте начинается там, где есть вот такой дизайн статус-шапки с информацией о событии:
Аккуратно свёрстано и работает. Во время вёрстки придумал, что для очного формата элемент с адресом будет кликабельным и открывать Яндекс.Карты с прокинутым в поиск, адресом. Урлы Яндекс.Карты умеют получать адрес и искать просто из ссылки с параметром ?text=
:
<!-- Параметр `z=16` — это масштаб карты (zoom), z=20, например покажет отдельное здание --> <a href="https://maps.yandex.ru/?text=Санкт-Петербург, Тверская 10&z=16" target="_blank" title="Подскажем адрес на Яндекс.Картах"> Санкт-Петербург, ... </a>
Включил это для тач-устройств в первую очередь: опыт дружелюбнее. В большинстве случаев после тапа, сразу откроется приложение Яндекс.Карт на телефоне. Если приложения на устройстве нет, то откроется браузер с сайтом карт сразу с адресом.
Захотелось также сделать что-то приятно-незаметное (пока не кликнешь, не знаешь, что о тебе позаботились и сценарий продумали уже) и для элемента с датой. Самый очевидный вариант — добавлялка напоминания в календарь.
А как у других ¶
Afisha.ru, russpass.ru, KudaGo и Яндекс Афиша ¶
На самых популярных порталах добавление в календарь не встречается в интерфейсе вовсе. Скорее там везде один паттерн: призыв к добавлению будет после действия — записи или покупки.
Также, как и с билетами на самолёт, например. В письме с маршрутной квитанцией обязательно будут ссылки, типа «Добавить в Apple Wallet» и добавлялки в календари.
Так что актуальных примеров для вдохновения я там не нашёл. Поискал скрины и в своих письмах по истории покупок билетов, понятны стали самые популярные варианты календарей:
- Google Calendar,
- iCal (Apple),
- Outlook Calendar,
- Есть ещё частные случаи, где компания активно пушит свои продукты: Календарь Mail.ru, Яндекс Календарь и проч.
TimePad ¶
Подглядел в TimePad. Если смотреть их стандартную «Афишу» — клик по блоку с датой скроллит к этажу выбора билетов. А вот идея с оформлением даты, как календарика — понравилась. Кнопок-действий для добавления в календарь на странице отдельного события нет (апрель, 2024). Но есть в письмах после регистрации или оплаты:
А если смотреть на страницу отдельного организатора мероприятий, то в блоке даты есть ссылка «Добавить в календарь» с выпадайкой и выбором вариантов. Любопытно, что такого нет на других отдельных страницах.
Как хочу сделать я ¶
Технические вводные у задачи: WordPress, PHP, свой шаблон и полный контроль над кодом, часть данных из админки доставать надо через поля Advanced Custom Fields (ACF).
- Я выбираю только Google Calendar и (Календаря iCloud) iCal (Apple) варианты из мысли о том, что уж они всегда в телефоне и под рукой.
- Придумал, что спрячу ссылки на добавление в dropdown по клику на дату: что элемент интерактивный и кликабельный, покажу ховер-эффектом.
- Чего ещё хочу: чтобы иконки календарей в списке показывали актуальную дату события в стилистике самой иконки. Как фавикон у гугла или иконка на рабочем столе в айфоне. Контекст события и всех данных о нём у меня есть в шаблоне, почему бы и не сверстать.
- Ещё хочу отдельную RSS-ленту для ical-календаря, чтобы на неё можно было подписаться и видеть весь актуальный список мероприятий.
Google Calendar ¶
У продукта Google Calendar квадратная по пропорциям иконка и внутри текстом указывается актуальный день месяца. В favicon calendar.google.com — тоже подставляется актуальный день, например, такое:
<link
rel="icon"
id="favicon"
type="image/x-icon"
href="https://calendar.google.com/googlecalendar/images/favicons_2020q4/calendar_27.ico"
>
Где «27
» в имени файла calendar_27.ico
— день месяца. Ещё очень интересна структура УРЛа. Что ж там такое ещё есть в favicons_2020q4
.
Как добавить событие Google Calendar в календарь ¶
Чтобы добавить какое-то событие в календарь — была (!) ссылка http://www.google.com/calendar/render?
с обширным списком параметров для события. Почему-то из официальной документации хоть какая-то полезная информация была удалена и все всегда во всех вопросах ссылаются на вебархивную ссылку из 2012-го (!): Share your events with an individual, a group, or the whole world.
Интернет очень странная штука, конечно. На этой же архивной странице есть генератор ссылок. Работает и спустя 12 лет.
А современные официальные ссылки документации и раздела помощи гугла показывают что-то бесполезное.
Параметры описаны на странице Instructions for making Google Calendar event reminder buttons, тоже вебархивная ссылка. Там параметры обозначают термином «CGI parameters».
Базовый набор:
# тут всегда только TEMPLATE, так сказано в доке action=TEMPLATE # название события text="Публикация поста и ссылках на календари" # местоположение location="https://sglazov.ru" # описание, можно простые HTML-теги details="Я публикую пост, это торжественно!" # дата, в Ymd\THis + `Z` после времени dates=20240517T190000Z/20240517T200000Z
И всё это можно превратить в набор параметров для ссылки, правда ссылка-пример из этой самой [документации] — не работает, почему-то в параметрах запроса есть лишние символы точки с запятой: text;=
, &details;=
ну и т. п. А конструктор кнопок — работает, но выбор года там только до 2020.
Ну и актуальный УРЛ для добавления мероприятий и формирования полной ссылки — https://calendar.google.com/calendar/r/eventedit
, параметры передаются гетом.
Беру и делаю ¶
SVG-иконку для фона взял случайную, сам в фигме удалил фон. При вёрстке поставил блок с текстом по центру размеров иконки и подогнал визуально размер шрифта, чтобы две цифры даты не поломали габариты блока. Ещё какое-то время понадобилось уделить тому, чтобы выяснить, параметр &ctz=Europe/Moscow
для часового пояса обязателен.
<?php // переменные $dateStart, $dateEnd, $speakerName, $title, $formatOffline, $city объявлены выше, там ничего интересного // форматирует даты $calendarDataStart = date('Ymd\THis', strtotime( $dateStart )); $calendarDataEnd = date('Ymd\THis', strtotime( $dateEnd )); $calendarDetails = "Преподаватель: {$speakerName}. Описание и программа: https://site.tld/events/" . get_the_id() . "/"; $calendarUrl = 'https://calendar.google.com/calendar/r/eventedit'; $calendarUrl .= '?text=' . htmlspecialchars( $title ); // название мероприятия $calendarUrl .= '&ctz=Europe/Moscow'; // часовой пояс обязательный $calendarUrl .= '&dates=' . $calendarDataStart . '/' . $calendarDataEnd; $calendarUrl .= '&details=' . htmlspecialchars($calendarDetails); // если событие в оффлайне и в админке указан город if ( $formatOffline && $city ) { $calendarAddress = $city; // добавляем параметр `location` с адресом к урлу календаря $calendarUrl .= '&location=' . htmlspecialchars($calendarAddress); } ?> <li class="dropdown-menu__item"> <a class="add-to-calendar-link" href="<?= $calendarUrl; ?>" title="Календарь откроется в новом окне" target="_blank"> <div class="add-to-calendar-link__symbol"> <svg role="img" width="22" height="22"> <use xlink:href="#i_google-calendar-blank"></use> </svg><!-- /.w-icon__symbol --> <div class="add-to-calendar-link__day"> <?= date('j', strtotime( $dateStart ) ); ?><!-- день месяца в стилизованной иконке календаря --> </div><!-- /.add-to-calendar-link__day --> </div><!-- /.add-to-calendar-link__symbol --> <div class="add-to-calendar-link__text"> Google Календарь </div><!-- /.add-to-calendar-link__text --> </a><!-- /.add-to-calendar-link --> </li><!-- /.dropdown-menu__item -->
И всё прекрасно заработало.
Ссылка для добавления в iCal — календарь Apple iCloud ¶
Стоит упомянуть, что «i» в названии формата iCalendar, созданного в 1998/2009, не имеет отношение к манере Apple называть свои продукты. В случае с iCalendar — это Internet Calendaring and Scheduling Core Object Specification (iCalendar).
Формат представляет собой набор параметров вида ключ-значение. В этой реализации все параметры орут и написаны прописными. В 199* так было модно? Файл на десятка два событий, визуально превращается в мешанину из капса, цифр и неожиданных двоеточий.
Пример структуры для отдельного события:
BEGIN:VCALENDAR PRODID:-//Timepad Ltd.//NONSGML Timepad//RU VERSION:2.0 METHOD:PUBLISH BEGIN:VEVENT UID:3414132849538 DTSTART:20240517T190000 DTEND:20240517T200000 DTSTAMP:20240415T000102 SUMMARY:SPB Frontend Митап #36 DESCRIPTION:[...] традиционная встреча веб-разработчиков в Санкт-Петербурге [...] LOCATION:Санкт-Петербург, Цветочная улица, дом 19 END:VEVENT END:VCALENDAR
Формат состоит из компонент-секций BEGIN:-END:
. Версия является обязательной и всегда указывается вторая. Сомневаюсь, что она будет когда-то меняться, но поскольку есть совсем старинная первая, то указывается вторая.
Вместо TITLE
использовали вариант SUMMARY
, ну ок. METHOD:PUBLISH
— это про событие, которое явно случится.
Отдельных VEVENT
внутри *.ics
может быть произвольное количество, не только одно событие.
Как всё работает и как должно работать у меня, тезисно: ¶
- Магией создаётся/генерируется налету файл
id.ics
, гдеid
— уникальный ID мероприятия, ну можно префикс ещё добавить; - К этому «файлу» можно обратиться по уникальной ссылке;
- В момент обращения — скачивается
*.ics
; - Файл при открытии запускает календарь, например, на айфоне;
- Или пользователь его открывает сам после скачивания (нууу, такоооое, но именно так оно задумано и работает).
Ещё я хотел отдельную ленту всех мероприятий в формате iCal. Придумал, что можно сделать кастомную RSS-ленту в WordPress, которая может принимать на вход один ID параметром, типа ?event=<?= get_the_id(); ?>
и формировать файл для отдельного мероприятия. Ну и без параметра урл вида /feed/ical/
будет отдавать всегда актуальный общий список мероприятий в формате iCalendar. Два в одном типа.
Кастомную RSS-ленту можно собрать через WordPress-хук add_feed()
. Писал про этот хук на Хабре в 2019-м: Кастомная авторассылка в MailChimp из RSS-ленты.
Скачать файл сразу поможет и сам браузер, но лучше ему помочь и указать заголовок Content-Disposition: attachment;
при ответе.
Создание RSS-ленты ¶
Файл feed-ical.php
:
// Добавляет новую ленту `/feed/ical` add_action('init', 'iCalendarFeed'); function iCalendarFeed() { add_feed('ical', 'iCalendarFeedTemplate'); } // Шаблон ленты function iCalendarFeedTemplate() { get_template_part('feed', 'ical'); }
Подключить в файле функций темы functions.php
:
// лента для Apple Календаря require get_template_directory() . '/core/custom/feed-ical.php';
Ну, у меня оно так разложено по своей структуре и собирается через require
под полным моим контролем.
Кнопка-действие ¶
<li class="dropdown-menu__item"> <a class="add-to-calendar-link" href="<?= get_feed_link('ical'); ?>?event=<?= get_the_id(); ?>" title="Файл события скачается" target="_blank" download="event-<?= get_the_id(); ?>.ics"> <div class="add-to-calendar-link__symbol"> <div class="apple-calendar"> <div class="apple-calendar__week"> <?= date_i18n('D', strtotime( $dateStart ) ); ?> </div><!-- /.apple-calendar__week --> <div class="apple-calendar__date"> <?= date('j', strtotime( $dateStart ) ); ?> </div><!-- /.apple-calendar__date --> </div><!-- /.apple-calendar --> </div><!-- /.add-to-calendar-link__symbol --> <div class="add-to-calendar-link__text"> iCal (Календарь Apple) </div><!-- /.add-to-google-calendar__text --> </a><!-- /.add-to-calendar-link --> </li><!-- /.dropdown-menu__item -->
WordPress-функция date_i18n()
позволяет из коробки работать с локализацией дат без магии. Ну и формат D
выведет сокращённый двухбуквенный вариант дня недели. Как в иконке календаря на айфоне. А на макос иконка позывает месяц + день.
Шаблон iCal-ленты ¶
<?php // имя файла $file = urlencode( "event-" . date('Y-m-d') . ".ics" ); // это — календарь, в UTF-8 header('Content-type: text/calendar; charset=utf-8'); // и файл надо скачать header("Content-Description: File Transfer"); // имя этому файлу объявлено header("Content-Disposition: attachment; filename={$file}"); // помогает всегда отдавать только свежее, игнорируя кэш header("Expires: 0"); // если `event=ID`, то карточку собираем для него if ( isset($_GET['event']) && $_GET['event'] ) { $getEvent = $_GET['event']; } $args = array( 'p' => $getEvent, // ID мероприятия из запроса 'post_type' => 'events', // мероприятия 'posts_per_page' => -1, // все ); $getEvents = new WP_Query($args); // в стандартном цикле WP перебираем всё, что нужно if ( $getEvents->have_posts() ) : ?> BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//RU NAME:БРЕНД X-WR-CALDESC:Краткое описание календаря TZID:Europe/Moscow <?php // в цикле собирает отдельный компонент `BEGIN:VEVENT-END:VEVENT` while ( $getEvents->have_posts() ) : // ... тут из acf/wp достаются нужные поля ob_start(); ?> BEGIN:VEVENT SUMMARY:<?= $title; ?> UID:<?= uniqid(); ?> DTSTAMP:<?= $timestamp; ?> DTSTART:<?= $dateStart; ?> DTEND:<?= $dateEnd; ?> LOCATION:<?= escapeString($location); ?> DESCRIPTION:<?= escapeString($description); ?> END:VEVENT <?php endwhile; ?> END:VCALENDAR <?php ob_end_clean(); return ob_get_contents(); endif; ?>
Стандарты и спецификации ¶
В 1996-м году написали спеку vCalendar, где «v» — virtual. vCalendar — формат данных для представления и обмена информацией календаря. В той же спеке было и первое упоминание формата vCard — формата данных для карточки контакта. vCard частично работает до сих и формат 1996 поддерживается в рамках обратной совместимости.
Формат vCalendar позже превратился в iCalendar — vCalendar: The Basis for Cross-Platform Scheduling. В 1998 году появилась спека RFC 2445.
В 2009-м, через два года после выхода первого iPhone, первую спеку прокачали для применения уже в разросшемся вебе — RFC 5545, она и является актуальной. Ещё её правили в 2016-м и в целом, спека — живая.
В 2014 году сделали мапинг формата iCalendar в json
-формат в спеке RFC 7265 — jCal. Перед ним в период популярности XML
была спека RFC 6321 — xCal, но формат уж слишком многословный получился.
Ещё всякие ссылки про календари и спецификации ¶
- Спецификации vCard и vCalendar
- Спецификации RFC 2445 и RFC 5545
- Сайт iCalendar.org
- «Calendaring Standards for Free Software» Andrew McMillan, 2014 год
- «Как работать с календарями» — Доклад Алексея Авдеева, FrontendConf