Подписка на новые статьи выглядит как одно поле и кнопка. На деле в ней четыре места, где легко ошибиться: чужой адрес, ссылки из письма на кэшируемом сайте, повторная рассылка при правке статьи и отписка.
Это 14-я часть цикла «WordPress на кэшируемом сайте».
1. Двойное подтверждение
Если подписывать адрес сразу, любой может подписать на ваш блог чужую почту — и вы будете слать письма человеку, который их не просил. Письма уйдут в спам, а репутация домена отправителя упадёт.
Поэтому после формы адрес попадает в базу со статусом «ждёт подтверждения», а на почту уходит письмо со ссылкой:
Кто-то (надеемся, вы) подписал этот адрес на уведомления о новых статьях блога.
Подтвердить подписку:
https://сайт/blog/?subscribe=confirm&t=<32 случайных символа>
Если это были не вы — просто проигнорируйте письмо, без подтверждения писем не будет.
Форма подписки защищена так же, как остальные: ловушка, время заполнения, не больше 5 попыток в час с адреса — иначе её используют, чтобы засыпать чужой ящик письмами подтверждения.
2. Ссылки из письма мимо кэша
Ссылка «подтвердить» ведёт на обычную страницу блога с параметрами. Страница блога кэшируется. Если ответ на эту ссылку попадёт в кэш, ничего страшного не случится сразу, но правильно, чтобы такие ответы в кэш не попадали вовсе: это действие, а не страница.
Обычный способ — nocache_headers() — здесь не сработает. В нашей конфигурации nginx игнорирует заголовок Cache-Control от PHP:
fastcgi_ignore_headers Cache-Control Expires;
Так сделано намеренно: WordPress и плагины любят слать «не кэшировать» на обычных страницах, и без этой строки кэш бы почти не наполнялся. Зато nginx слушает свой заголовок X-Accel-Expires:
add_action('template_redirect', function () {
$action = $_GET['subscribe'] ?? '';
$token = preg_replace('~[^A-Za-z0-9]~', '', (string) ($_GET['t'] ?? ''));
if (!in_array($action, ['confirm', 'stop'], true) || strlen($token) !== 32) {
return;
}
// … найти подписчика по токену, сменить статус, запомнить сообщение для плашки
nocache_headers();
header('X-Accel-Expires: 0'); // nginx: этот ответ не сохранять
});
Проверка: два запроса подряд на ссылку подтверждения — оба MISS, то есть ответ ни разу не сохранился.
3. Рассылка только при первой публикации
- рассылку запускает переход статьи в «опубликовано» из любого другого статуса;
- в мету статьи сразу ставится отметка «в очереди», после завершения — время рассылки. Правка опубликованной статьи и повторная публикация после снятия рассылку не повторяют;
- рассылка идёт кроном через минуту после выхода, пачками по 40 адресов, следующая пачка — ещё через минуту. Публикация не ждёт рассылку, а почтовый сервер не получает сотни писем разом.
add_action('transition_post_status', function ($new, $old, $post) {
if ($post->post_type === 'post' && $new === 'publish' && $old !== 'publish'
&& !get_post_meta($post->ID, '_subscribers_notified', true)) {
update_post_meta($post->ID, '_subscribers_notified', 'queued');
wp_schedule_single_event(time() + MINUTE_IN_SECONDS, 'subscribe_send', [$post->ID, 0]);
}
}, 10, 3);
Важно, что крон вообще работает: на сайте со страницами из кэша встроенный запуск почти не срабатывает (вторая часть).
4. Отписка в одно нажатие
В каждом письме — ссылка «отписаться» с тем же токеном и заголовок List-Unsubscribe: почтовые сервисы показывают по нему свою кнопку отписки рядом с отправителем. Человек, который нашёл, как отписаться, не нажимает «это спам».
Что пока не сделано — честно
- подтверждение по простой ссылке. Некоторые почтовые фильтры сами открывают ссылки из писем, чтобы проверить их на вредоносность, — и такой фильтр подтвердит подписку без человека. Надёжнее страница подтверждения с кнопкой, которая отправляет POST;
- нет заголовка
List-Unsubscribe-Postдля отписки в один клик по стандарту, который крупные почтовые сервисы требуют от массовых рассылок. Пока подписчиков немного, это не критично; при росте — добавить.
Нужна подписка на обновления сайта, которая не попадает в спам?
Пришлите адрес — сделаю подтверждение, отписку и рассылку пачками.
// загружаю комментарии…