Если внешний счётчик временно недоступен, владелец сайта может сохранить хотя бы базовую статистику просмотров в собственной базе данных. Для этого необязательно писать полноценную систему аналитики. Достаточно небольшого JavaScript-фрагмента, PHP-обработчика и таблицы MySQL.
Важное ограничение: пример ниже считает просмотры страниц, а не уникальных людей и не сеансы. Мы не будем выдавать каждую запись за отдельного посетителя. Он подходит для обучения и небольшого собственного проекта после настройки защиты.
Браузер отправляет просмотр на свой PHP-endpoint, который сохраняет запись в MySQL.
Какие данные будем собирать
- время просмотра;
- адрес страницы без query-параметров;
- домен источника перехода, если браузер его передал.
IP, идентификаторы пользователей, cookies, значения форм и полные URL с UTM-параметрами в примере не сохраняются. Это снижает объём данных, но не отменяет необходимость соблюдать применимые требования к обработке данных.
Шаг 1. Создаём таблицу MySQL
Используйте отдельную таблицу в базе, доступ к которой есть только у приложения:
CREATE TABLE site_pageviews (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
path VARCHAR(500) NOT NULL,
referrer_host VARCHAR(190) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY created_at (created_at),
KEY path_prefix (path(190))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Индекс времени ускоряет отчёты по дням. Для больших объёмов понадобится пересмотреть структуру и агрегирование; не превращайте основную базу MODX в бесконечный журнал событий.
Шаг 2. Отправляем просмотр из браузера
Добавьте код один раз в общий шаблон, после выполнения необходимых условий согласия на аналитику. sendBeacon подходит для простой отправки фонового события, а fetch используется как запасной путь:
<!-- Add only where analytics processing is legally permitted. -->
<script>
(function () {
var payload = {
path: location.pathname,
referrer: document.referrer || ''
};
var blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics/collect.php', blob);
} else {
fetch('/analytics/collect.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
credentials: 'same-origin',
keepalive: true
}).catch(function () {});
}
})();
</script>
Код не определяет «уникального посетителя». Если человек обновил страницу, будет ещё один просмотр. Если бот выполняет JavaScript, его просмотры тоже могут попасть в базу.
Шаг 3. Принимаем данные в PHP
Создайте endpoint /analytics/collect.php. Для подключения к базе используйте переменные окружения PV_DSN, PV_DB_USER и PV_DB_PASSWORD; например DSN: mysql:host=127.0.0.1;dbname=YOUR_DB;charset=utf8mb4. Пароли не должны попадать в веб-каталог или общедоступную статью.
// /public_html/analytics/collect.php — minimal educational example.
// Configure PV_DSN, PV_DB_USER and PV_DB_PASSWORD outside web root.
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); exit;
}
if ((int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 4096) {
http_response_code(413); exit;
}
header('Cache-Control: no-store');
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$host = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
$originHost = strtolower((string)(parse_url($origin, PHP_URL_HOST) ?: ''));
if ($origin && $originHost !== preg_replace('/:\\d+$/', '', $host)) {
http_response_code(403); exit;
}
$data = json_decode((string)file_get_contents('php://input'), true);
if (!is_array($data)) { http_response_code(400); exit; }
$path = (string)($data['path'] ?? '');
if (!$path || $path[0] !== '/' || substr($path, 0, 2) === '//') {
http_response_code(400); exit;
}
$path = mb_substr(strtok($path, "?#") ?: '/', 0, 500, 'UTF-8');
$refHost = strtolower((string)(parse_url((string)($data['referrer'] ?? ''), PHP_URL_HOST) ?: ''));
$refHost = preg_match('/^[a-z0-9.-]{1,190}$/', $refHost) ? $refHost : '';
try {
$pdo = new PDO(getenv('PV_DSN'), getenv('PV_DB_USER'), getenv('PV_DB_PASSWORD'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('INSERT INTO site_pageviews (path, referrer_host) VALUES (?, ?)');
$stmt->execute([$path, $refHost]);
http_response_code(204);
} catch (Throwable $e) {
error_log('Pageview insert failed');
http_response_code(503);
}
Это минимальная демонстрация. Проверка Origin защищает только от части браузерных сценариев — злоумышленник может отправить запрос напрямую. Для реального сайта добавьте ограничение частоты на уровне сервера, антибот-фильтры, контроль размера событий и мониторинг аномального потока.
Шаг 4. Строим первый отчёт
На первых порах можно вывести количество просмотров по дням обычным SQL. И сразу установить срок хранения сырых записей, например 90 дней:
-- Views per day (not unique visitors):
SELECT DATE(created_at) AS day, COUNT(*) AS views
FROM site_pageviews
WHERE created_at >= NOW() - INTERVAL 7 DAY
GROUP BY DATE(created_at) ORDER BY day;
-- Remove old raw events regularly (e.g. via a daily cron):
DELETE FROM site_pageviews WHERE created_at < NOW() - INTERVAL 90 DAY;
Получается независимый учёт просмотров, но не полноценная замена GA4 или Метрики.
Что учесть при установке на MODX
Код отправки можно поместить в отдельный чанк и подключить в общий шаблон MODX. В отличие от серверного сниппета, который исполняется при формировании HTML и может оказаться закешированным, browser-beacon выполняется при фактическом открытии страницы. Но пользователи с отключённым JavaScript не попадут в такую статистику.
Не подключайте обработчик напрямую к открытой MODX Console и не размещайте секреты в ресурсе или чанке. Для production лучше оформить отдельный контроллер/коннектор с защитами и не зависеть от общего кеша страниц.
Что этот счётчик не умеет
- не определяет уникальных посетителей;
- не восстанавливает прошлые визиты;
- не заменяет вебвизор, цели, атрибуцию рекламы и электронную торговлю;
- без доработок не защищён от накрутки и больших нагрузок.
Если нужен простой резерв без программирования, посмотрите пять бесплатных аналитических систем. Если важны коммерческие результаты, обязательно настройте отдельный учёт заявок и заказов. А для контроля доступности скрипта есть мониторинг с Telegram.


