feat: счётчики с consent, политика, фиксы дизайна

Дизайн-фиксы:
- Перенесены картинки из старого WP в public/wp-content/uploads/
  (header_bg.gif + 3 размера NY-baner.png 2014/12). migrate-wp.mjs
  обновлён: корректно обрабатывает <a><img></a>, переписывает абсолютные
  URL anotherreflections.ru/wp-content/... в относительные
- Description в frontmatter теперь чистый plain text — без markdown-разметки
- Слаги 11/95/152 → kto-my, s-23-fevralya-2011, s-nastupayushhim-novym-2012-godom
- Hero-метрика: «N лет онлайн» → «с 2006 года» (последний пост 2015,
  активной жизни «20 лет» нет)
- Drop cap (буквица) — только для постов длиннее 240 символов
  (короткие посты раньше выглядели обрезанными)

Аналитика и конфиденциальность:
- Яндекс.Метрика (counter 13938862, webvisor) и Google gtag (GT-PH39R3X)
  перенесены со старого WP
- Cookie consent banner — самописный, без зависимостей: счётчики грузятся
  только после явного «Принять». Выбор хранится в localStorage + cookie
  ar-consent на 12 месяцев. Уведомление в духе 152-ФЗ
- /privacy/ — полная политика конфиденциальности: что собираем (через
  Метрику и GA), для чего, как cookies устроены, права пользователя,
  контакт для запросов на удаление
- В футере добавлены ссылки «Политика конфиденциальности» и «Контакты»
- sitemap.txt + llms.txt включают /privacy/
This commit is contained in:
2026-05-21 02:07:08 +03:00
parent 864819a67c
commit 6f2cfdd63d
19 changed files with 310 additions and 13 deletions

View File

@@ -0,0 +1,73 @@
---
// Минимальный self-hosted cookie consent под 152-ФЗ:
// — баннер показывается до явного выбора (cookie `ar-consent`)
// — «Принять» → активирует все <script type="text/plain" data-cookieconsent="statistics">
// — «Только необходимые» → ничего не активирует, баннер исчезает
// — выбор хранится в localStorage и cookie на 12 месяцев
---
<div id="cookie-consent" class="cookie-consent" hidden>
<div class="cookie-text">
<p>
Мы используем cookies и сервисы статистики (Яндекс.Метрика, Google Analytics),
чтобы понимать, как вы пользуетесь сайтом, и делать его лучше.
Подробнее — в <a href="/privacy/">политике конфиденциальности</a>.
</p>
</div>
<div class="cookie-actions">
<button type="button" class="cookie-btn cookie-btn-secondary" data-cc-deny>Только необходимые</button>
<button type="button" class="cookie-btn cookie-btn-primary" data-cc-accept>Принять</button>
</div>
</div>
<script is:inline>
(function () {
const KEY = 'ar-consent';
const banner = document.getElementById('cookie-consent');
if (!banner) return;
const activate = () => {
document.querySelectorAll('script[type="text/plain"][data-cookieconsent="statistics"]').forEach((tpl) => {
const s = document.createElement('script');
for (const a of tpl.attributes) {
if (a.name === 'type' || a.name === 'data-cookieconsent') continue;
s.setAttribute(a.name, a.value);
}
s.text = tpl.textContent || '';
tpl.parentNode.insertBefore(s, tpl);
tpl.parentNode.removeChild(tpl);
});
};
const setConsent = (value) => {
try { localStorage.setItem(KEY, value); } catch {}
const exp = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = `${KEY}=${value}; expires=${exp}; path=/; SameSite=Lax`;
};
const current = (() => {
try {
const ls = localStorage.getItem(KEY);
if (ls) return ls;
} catch {}
const m = document.cookie.match(/(?:^|; )ar-consent=([^;]+)/);
return m ? decodeURIComponent(m[1]) : null;
})();
if (current === 'accept') {
activate();
} else if (current === 'deny') {
// ничего не делаем — статистика не активируется
} else {
banner.hidden = false;
}
banner.querySelector('[data-cc-accept]')?.addEventListener('click', () => {
setConsent('accept');
banner.hidden = true;
activate();
});
banner.querySelector('[data-cc-deny]')?.addEventListener('click', () => {
setConsent('deny');
banner.hidden = true;
});
})();
</script>