Tilda · 8 мин чтения

Форма для отзывов Тильда: как принимать отзывы с модерацией

Форма для отзывов Тильда собирается из обычного блока формы, вебхука и небольшого PHP-обработчика на вашем хостинге. Отзыв сначала попадает в таблицу MySQL со статусом «на проверке», вам приходит уведомление в Telegram, а на сайте показываются только одобренные записи. Такую схему я ставлю там, где нельзя пускать текст на страницу без проверки: спам со ссылками, отзывы конкурентов, жалобы без заказа.

Как устроена форма приёма отзывов с проверкой перед публикацией

Цепочка короткая:

  1. Посетитель заполняет форму в блоке Тильды: имя, оценка, текст.
  2. Тильда отправляет заявку вебхуком на PHP-скрипт.
  3. Скрипт сохраняет отзыв в MySQL со статусом pending и шлёт вам сообщение в Telegram со ссылкой на страницу проверки.
  4. Вы открываете ссылку, читаете отзыв и нажимаете «Опубликовать» или «Отклонить».
  5. Блок на странице отзывов запрашивает у другого скрипта список одобренных записей и рисует карточки.

Вебхук доступен на тарифе Personal и выше, отдельной доплаты за него нет. PHP и MySQL уже входят в обычный хостинг, VPS под это не нужен. Если отзывов мало, можно обойтись формой с отправкой на почту и вставлять одобренные тексты в блок вручную. Схема ниже окупается, когда отзывы идут регулярно и каждый раз копировать их руками неудобно.

Подход Что происходит с отзывом Когда подходит
Форма с отправкой на почту, ручная вставка Вы копируете текст в блок и публикуете страницу Несколько отзывов в месяц
Вебхук, PHP и MySQL Отзыв хранится в базе, публикуется кнопкой, страница обновляется без переопубликации Отзывы идут постоянно, нужна проверка
Сторонний сервис отзывов Хранение и модерация на стороне сервиса Нужны готовые функции сервиса, а контакты клиентов вы готовы передавать ему

Стоимость хостинга и домена в таблицу не вынесена намеренно: она не зависит от выбранного подхода, вы платите за них в любом случае.

Как настроить форму для сбора отзывов в Тильде

Берёте любой блок с формой из категории «Форма» и оставляете три поля. Имена переменных полей задаёте в «Контенте» блока: Name для имени, Rating для оценки, Review для текста. Для оценки подойдёт поле с вариантами от 1 до 5, например радиокнопки. Эти имена потом читает PHP-скрипт, поэтому писать их нужно латиницей и без пробелов.

Дальше подключаете вебхук. Адрес скрипта указывается один раз на уровне сайта: «Настройки сайта → Формы → Webhook». В блоке с формой в «Контенте» отмечаете сервис Webhook. Здесь есть три нюанса из справки Тильды, которые на практике и ломают интеграции:

  • Тело запроса приходит как обычный form-urlencoded POST, а не JSON. В PHP читать нужно $_POST, а не php://input с json_decode.
  • Адрес обработчика должен быть на HTTPS и отвечать за пять секунд. Иначе Тильда сделает ещё две попытки с интервалом в минуту, и один отзыв может записаться трижды.
  • Сразу после подключения Тильда шлёт тестовый запрос с одним полем test=test и ждёт ответ 200. Скрипт обязан такой запрос принять и не падать.

Для защиты от чужих запросов в настройках приёмщика задаётся ключ API: имя, значение и способ передачи. Я передаю его как POST-параметр tkey и проверяю в скрипте. Адрес обработчика посторонние не знают, но защита по одному секрету быстро перестаёт быть секретом, поэтому ключ нужен обязательно.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

PHP-обработчик и таблица отзывов в MySQL

Сначала таблица. Статус по умолчанию pending, поэтому новый отзыв публично не виден, пока вы его не одобрите. Поле token нужно для ссылки модерации.

CREATE TABLE reviews (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  text TEXT NOT NULL,
  rating TINYINT NOT NULL,
  status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
  token CHAR(32) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_status_date (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Общие настройки лежат в config.php. Токен бота берётся у @BotFather, а chat_id это ваш личный чат с ботом, в котором вы предварительно нажали «Старт».

<?php
const DB_DSN = 'mysql:host=localhost;dbname=base;charset=utf8mb4';
const DB_USER = 'user';
const DB_PASS = 'password';
const TG_TOKEN = '123456:token';
const TG_CHAT = '123456789';
const WEBHOOK_KEY = 'длинная-случайная-строка';
const BASE_URL = 'https://example.ru/reviews';

function db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    }
    return $pdo;
}

Теперь скрипт, который принимает вебхук. Он сначала отвечает Тильде, а уже потом пишет в Telegram, чтобы не упереться в пятисекундный лимит.

<?php
require __DIR__ . '/config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit;
}

// тестовый запрос Тильды (test=test) просто подтверждаем
if (!isset($_POST['Review'])) {
    exit('ok');
}

if (!hash_equals(WEBHOOK_KEY, (string)($_POST['tkey'] ?? ''))) {
    http_response_code(403);
    exit;
}

$name = mb_substr(trim(strip_tags((string)($_POST['Name'] ?? ''))), 0, 100);
$text = mb_substr(trim(strip_tags((string)($_POST['Review'] ?? ''))), 0, 2000);
$rating = max(1, min(5, (int)($_POST['Rating'] ?? 5)));

// короткие тексты и ссылки отсекаем молча, отвечая 200
if (mb_strlen($text) < 20 || preg_match('~https?://~i', $text)) {
    exit('ok');
}

$token = bin2hex(random_bytes(16));
$st = db()->prepare('INSERT INTO reviews (name, text, rating, token) VALUES (?, ?, ?, ?)');
$st->execute([$name, $text, $rating, $token]);
$id = (int)db()->lastInsertId();

echo 'ok';
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
}

$msg = 'Новый отзыв #' . $id . ', оценка ' . $rating . PHP_EOL . $name . PHP_EOL . PHP_EOL . $text
    . PHP_EOL . PHP_EOL . BASE_URL . '/moderate.php?id=' . $id . '&token=' . $token;

$ch = curl_init('https://api.telegram.org/bot' . TG_TOKEN . '/sendMessage');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => ['chat_id' => TG_CHAT, 'text' => $msg, 'disable_web_page_preview' => 'true'],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 3,
]);
curl_exec($ch);

После подключения отправьте тестовый отзыв и загляните в таблицу. Справка Тильды предупреждает, что часть символов в заявках приходит экранированной, например вместо @ может прийти %40. Если в базе вы увидите такие последовательности, оберните значения в urldecode(). Ещё одна проверка: ваш хостинг должен выходить на api.telegram.org. Если исходящее соединение закрыто, уведомление можно отправлять письмом через mail(), остальная схема не меняется.

Модерация отзывов: кнопки «Опубликовать» и «Отклонить»

Ссылку из Telegram нельзя делать прямым действием по GET: мессенджеры и антивирусы подгружают ссылки для превью, и отзыв одобрится сам. Поэтому по ссылке открывается страница с текстом отзыва и двумя кнопками, а статус меняет только POST. Токен в ссылке случайный, перебрать его нельзя.

<?php
require __DIR__ . '/config.php';
header('Content-Type: text/html; charset=utf-8');

$id = (int)($_REQUEST['id'] ?? 0);
$token = (string)($_REQUEST['token'] ?? '');

$st = db()->prepare('SELECT * FROM reviews WHERE id = ?');
$st->execute([$id]);
$r = $st->fetch(PDO::FETCH_ASSOC);

if (!$r || !hash_equals($r['token'], $token)) {
    http_response_code(404);
    exit('Не найдено');
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $status = ($_POST['action'] ?? '') === 'approve' ? 'approved' : 'rejected';
    db()->prepare('UPDATE reviews SET status = ? WHERE id = ?')->execute([$status, $id]);
    exit('Готово: ' . $status);
}
?>
<p><?= htmlspecialchars($r['name']) ?>, оценка <?= (int)$r['rating'] ?> из 5</p>
<p><?= nl2br(htmlspecialchars($r['text'])) ?></p>
<form method="post">
  <button name="action" value="approve">Опубликовать</button>
  <button name="action" value="reject">Отклонить</button>
</form>

Форма отправляется на тот же адрес вместе с параметрами id и token из строки запроса, поэтому отдельный action не нужен. Если хотите отменять решение, добавьте кнопку, возвращающую статус pending. На практике я чаще оставляю только два действия, чтобы не усложнять.

Вывод одобренных отзывов на странице Тильды

Публичный скрипт отдаёт только записи со статусом approved. В заголовке Access-Control-Allow-Origin указывается домен вашего сайта на Тильде, потому что запрос идёт с одного домена на другой.

<?php
require __DIR__ . '/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: https://example.ru');
header('Cache-Control: public, max-age=300');

$sql = "SELECT name, text, rating, created_at FROM reviews WHERE status = 'approved' ORDER BY created_at DESC LIMIT 50";
echo json_encode(db()->query($sql)->fetchAll(PDO::FETCH_ASSOC), JSON_UNESCAPED_UNICODE);

На странице отзывов добавляете блок T123 («Библиотека блоков → Другое») и вставляете код. Текст кладётся через textContent, а не через innerHTML: даже если в базу как-то попадёт разметка, на странице она останется обычным текстом.

<div id="reviews-list"></div>
<script>
fetch('https://example.ru/reviews/reviews.php')
  .then(function (r) { return r.json(); })
  .then(function (items) {
    var box = document.getElementById('reviews-list');
    items.forEach(function (it) {
      var card = document.createElement('div');
      card.className = 'review-card';
      var head = document.createElement('strong');
      head.textContent = it.name + ' ' + '★'.repeat(it.rating);
      var body = document.createElement('p');
      body.textContent = it.text;
      card.appendChild(head);
      card.appendChild(body);
      box.appendChild(card);
    });
  });
</script>

Стили для .review-card добавляются в этот же блок тегом style или в общий CSS сайта. Преимущество такого вывода в том, что одобренный отзыв появляется на странице без переопубликации: кеш скрипта держится 5 минут, после чего список обновится у всех посетителей.

Защита от спама и накрутки в форме отзывов

Скрипт выше уже режет самое частое: тексты короче 20 символов и сообщения со ссылками. Сам факт модерации закрывает остальное, потому что спам не доходит до публичной страницы. Что я добавляю сверху:

  • Ограничение по IP не работает. Запросы приходят с серверов Тильды, а не от посетителя, поэтому лимит по адресу заблокирует всех сразу. Вместо этого можно ограничивать число отзывов с одним email или телефоном в сутки: добавьте в форму поле контакта и проверяйте его в скрипте перед вставкой.
  • Дубли из-за повторных попыток. Если обработчик когда-нибудь ответит позже пяти секунд, Тильда отправит заявку ещё раз. Помогает уникальный индекс по хешу пары «имя + текст» и INSERT IGNORE.
  • Подтверждение заказа. Для магазина отзывы без покупки часто бесполезны. Номер заказа можно сверять с тем, что приходит из корзины или CRM, но это уже зависит от вашей учётной системы.
  • Фото к отзыву. Поле «Файл» есть не во всех блоках, а только в части вертикальных форм BF. Оно требует подключённого облачного хранилища, размер файла до 30 МБ. Ссылку на файл потом нужно проверять так же, как текст.

Если нужны ответы владельца под отзывом, привязка к товару из каталога или фильтрация по оценке, это уже кастомные доработки Тильды. Простую доработку скрипта я оцениваю от 3 000 ₽, цена растёт вместе с числом сценариев.

Условия и возможности Тильды и Telegram меняются, поэтому перед запуском я уточняю их у самих сервисов.

Частые вопросы

На каком тарифе Тильды заработает такая форма отзывов?

Нужен платный тариф: вебхук и вставка своего HTML-кода доступны на Personal и Business. На Free подключить свой сервис через вебхук нельзя.

Можно ли обойтись без PHP и своего хостинга?

Можно, если принимать отзывы формой с отправкой на почту и добавлять одобренные вручную в обычный блок. Для автоматической публикации нужно место, где хранятся отзывы и откуда их забирает страница. Обычного хостинга с PHP и MySQL для этого достаточно, отдельный сервер не требуется.

Что делать, если отзыв записался несколько раз?

Тильда повторяет вебхук дважды с интервалом в минуту, если обработчик не ответил за пять секунд. Отвечайте сразу, как в примере выше, а тяжёлые действия делайте после ответа. Дополнительно можно поставить уникальный индекс по хешу содержимого отзыва.

Как показать оценку звёздами и средний рейтинг?

Оценка хранится числом от 1 до 5, поэтому звёзды рисуются на странице повторением символа, как в блоке T123 выше. Средний рейтинг считается запросом AVG(rating) по одобренным записям, его можно отдавать из того же публичного скрипта отдельным полем.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно