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

@@ -28,15 +28,22 @@ const decodeEntities = (s) => s
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
/** Заменить старые WP-uploads URL на относительные (мы скачали в public/wp-content/uploads/). */
const rewriteWpUploads = (url) =>
url.replace(/^https?:\/\/anotherreflections\.ru(\/wp-content\/uploads\/.+)$/i, '$1');
const htmlToMd = (html) => {
if (!html) return '';
let s = html;
// images
// Сначала "вложенные" <a href="..."><img src="..."></a> — превращаем в один markdown-image со ссылкой
s = s.replace(/<a\s+[^>]*?href=["']([^"']+)["'][^>]*>\s*<img[^>]*?src=["']([^"']+)["'][^>]*?(?:alt=["']([^"']*)["'])?[^>]*?\/?>\s*<\/a>/gi,
(_, href, src, alt) => `[![${alt || ''}](${rewriteWpUploads(src)})](${rewriteWpUploads(href)})`);
// одиночные images
s = s.replace(/<img[^>]*?src=["']([^"']+)["'][^>]*?(?:alt=["']([^"']*)["'])?[^>]*?\/?>/gi,
(_, src, alt) => `![${alt || ''}](${src})`);
(_, src, alt) => `![${alt || ''}](${rewriteWpUploads(src)})`);
// links
s = s.replace(/<a\s+[^>]*?href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
(_, href, text) => `[${text.replace(/<[^>]+>/g,'').trim()}](${href})`);
(_, href, text) => `[${text.replace(/<[^>]+>/g,'').trim()}](${rewriteWpUploads(href)})`);
// bold
s = s.replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
// italic
@@ -106,7 +113,12 @@ for (const p of data.posts) {
categories: p.categories.map(c => c.name),
categorySlugs: p.categories.map(c => c.slug),
tags: p.tags.map(t => t.name),
description: (htmlToMd(p.excerpt) || htmlToMd(p.content_html)).slice(0, 200).replace(/\s+/g,' ').trim(),
description: (htmlToMd(p.excerpt) || htmlToMd(p.content_html))
.replace(/!?\[([^\]]*)\]\([^)]+\)/g, '$1') // strip markdown images/links для excerpt
.replace(/[*_>#]+/g, '') // strip md formatting символов
.replace(/\s+/g, ' ')
.trim()
.slice(0, 200),
};
const body = htmlToMd(p.content_html);
const out = `${fmtFrontmatter(fm)}\n\n${body}\n`;