惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

PCI Perspectives
PCI Perspectives
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Palo Alto Networks Blog
S
Schneier on Security
Scott Helme
Scott Helme
T
Threat Research - Cisco Blogs
K
Kaspersky official blog
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tenable Blog
G
GRAHAM CLULEY
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
I
Intezer
D
Docker
月光博客
月光博客
L
Lohrmann on Cybersecurity
Latest news
Latest news
B
Blog
罗磊的独立博客
M
MIT News - Artificial intelligence
S
Securelist
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
Recorded Future
Recorded Future
S
SegmentFault 最新的问题
N
Netflix TechBlog - Medium
T
Threatpost
H
Hacker News: Front Page
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
人人都是产品经理
人人都是产品经理
F
Fortinet All Blogs
博客园 - Franky
P
Proofpoint News Feed
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
A
About on SuperTechFans
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tor Project blog
Google Online Security Blog
Google Online Security Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Engineering at Meta
Engineering at Meta
Webroot Blog
Webroot Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Microsoft Security Blog
Microsoft Security Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Lighthouse 100 / 100: как мы повесили GTM, GA4, Яндекс.Метрику и Clarity на статический сайт — и не уронили скорость
Тимур · 2026-06-01 · via Все публикации подряд на Хабре

Средний

12 мин

6.2K


Когда маркетологи хотят всё: сырые данные в GA4, запись сессий в Метрике, хитмапы в Clarity и при этом «Lighthouse 100» в PageSpeed Insights — приходится изобретать. Расскажу, как мы это сделали на небольшом проекте и во что это обошлось по времени и нервам.

Проект — нишевый агрегатор российских хостинг-провайдеров. Более 120 страниц в sitemap, 31 статья, десятки категорий услуг, живые цены, сравнения. Стек: Astro 6 + Strapi 5 + Tailwind 4, плюс Partytown, PostgreSQL, Nginx и обычный VPS на Ubuntu. Сайт собирается в статику во время билда, никакого SSR в рантайме нет.

На desktop — Lighthouse 100 / 100 / 100 / 100. На mobile с жёстким throttling (4x slow CPU) — 99 / 100 / 100 / 100. В реальных условиях и по Chrome UX Report — 100 везде. LCP на desktop — 0,5 секунды, на mobile throttled — 1,7 секунды. CLS — ноль. TBT — 10 ms на мобильном и 0 ms на десктопе.

Lighthouse на десктопе: 100 / 100 / 100 / 100. LCP 0,5 с, TBT 0 ms, CLS 0. Обычный прогон без throttling.

Lighthouse на десктопе: 100 / 100 / 100 / 100. LCP 0,5 с, TBT 0 ms, CLS 0. Обычный прогон без throttling.

Тот же сайт на mobile с жёстким throttling (4x slow CPU): 99 / 100 / 100 / 100. LCP 1,7 с, TBT 10 ms, CLS 0. У реальных пользователей в Chrome UX Report — 100 везде.

Тот же сайт на mobile с жёстким throttling (4x slow CPU): 99 / 100 / 100 / 100. LCP 1,7 с, TBT 10 ms, CLS 0. У реальных пользователей в Chrome UX Report — 100 везде.

Дальше — по порядку. Почему Astro, а не Next.js и не Nuxt. Как Strapi живёт рядом со статикой и почему нас чуть не похоронила одна его особенность. Две недели поиска причин, почему ломалась тёмная тема в Tailwind 4. И, главное, как Partytown вырвал 1 190 миллисекунд блокировки из главного потока и переписал нам результат PageSpeed Insights.


Почему Astro, а не Next.js или Nuxt

Сначала рассматривали Next.js 14 с App Router. Но для контентного сайта с 120+ страницами, где 90% контента — тексты, таблицы и карточки, SSR и гидратация React-компонентов были избыточны.

Astro дал три ключевых преимущества:

  • Zero-JS по умолчанию. Страница генерируется в чистый HTML на этапе сборки. JavaScript грузится только для интерактивных элементов — калькулятора, квиза — через Islands Architecture. HTML-страница весит 15-25 KB, а не 200+ KB с React-рантаймом.

  • Partial hydration. Компоненты с client:load или client:idle гидратируются изолированно. Остальная страница остаётся статической. Это критично для метрик — нет блокирующего парсинга скриптов в <head>.

  • Контент-фокус. Astro из коробки умеет работать с Markdown, MDX и любой CMS через loaders. Не пришлось городить костыли для динамических маршрутов вроде /catalog/[slug].astro.

// astro.config.mjs — минимальная конфигурация
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  output: 'static',
  prefetch: true,
  vite: {
    plugins: [tailwindcss()],
  },
});

Как Strapi v5 работает как Headless CMS для SSG

Стандартный паттерн: CMS на Node.js отдаёт JSON по REST, фронтенд запрашивает данные на каждый вход. У нас всё наоборот. Фронтенд ходит в Strapi только во время сборки. Всё остальное время Strapi живёт своей жизнью на PM2, отдаёт редакторам свой admin-UI, и сайту до него нет никакого дела.

// src/lib/strapi.ts — fetch на этапе build
export async function fetchProviders() {
  const res = await fetch(`${STRAPI_URL}/api/providers?populate=*`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
  const json = await res.json();
  return json.data || [];
}

В каждом .astro-файле данные запрашиваются в frontmatter-секции (server-side во время сборки). Результат — статический HTML с уже встроенными данными. Пользователю не нужно ждать API-запросов.

// src/pages/providers/[slug].astro
export async function getStaticPaths() {
  const providers = await fetchProviders();
  return providers.map((p) => ({
    params: { slug: p.slug },
    props: { data: p },
  }));
}

const { data } = Astro.props;

Strapi v5 с Document Service API и documentId вместо числовых id потребовал адаптации — мы написали обёртку, которая нормализует ответы (поддерживает и старый attributes-формат, и новый плоский). Главный плюс: Strapi управляет контентом, сложная бизнес-логика (affiliate-ссылки, pluralization, UTM) живёт во фронтенде.

Главные грабли, на которые наступили. После npm run build папка dist/ содержит только скомпилированный admin-интерфейс. Конфиг и schema она не копирует. Если забыть ручные cp -r config dist/config && cp -r src dist/src и рестартнуть PM2 — при следующем старте получишь пустой проект. Чуть не потеряли весь контент в тестовом окружении. С тех пор у нас железное правило: перед любым ребилдом Strapi — dump БД, после сборки — копирование config и src в dist.


Tailwind CSS 4: почему миграция съела времени впятеро больше ожидаемого

Проект стартовал на Tailwind 3.4. Когда вышел v4 с CSS-first конфигурацией, мы решили мигрировать — хотели избавиться от tailwind.config.js и перейти на нативные CSS-директивы.

/* src/styles/global.css — конфигурация Tailwind 4 */
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}

@custom-variant dark (&:is(.dark *));

Что сломалось:

  1. Scoped styles с @apply. В Astro <style> блоках @reference "tailwindcss" не наследовал @custom-variant dark. Компилировалось в @media(prefers-color-scheme:dark) вместо .dark. Полтора дня пытались понять, почему в layout компоненте class:list={['dark:bg-slate-900']} работает по клику кнопки темы, а в <style> рядом — нет. Пофиксили через @reference "../../styles/global.css" с правильным относительным путём — в доках этого нет, нашли по старым GitHub issues.

  2. Переменные карточек. Мы использовали CSS-переменные для темизации компонентов (--card-bg, --text-primary). Tailwind 4 с Vite-plugin иногда tree-shake'ил неиспользуемые переменные из :root — страница собиралась, в браузере выглядела как белый прямоугольник без фоновых цветов. Пришлось явно прописать все кастомные переменные в global.css и отдельно в селекторе .dark, иначе PostCSS-проход их выбрасывал.

  3. Классы вида dark:bg-[#1e293b]. Работают, но #1e293b — это не slate-900. Мы осознанно используем #1e293b для синего оттенка тёмной темы, и Tailwind 4 это не сломал, но важно следить за arbitrary values.

  4. Логотипы в тёмной теме. Провайдеры дают логотипы в двух вариантах — для светлого и тёмного фона. Сделали переключение через <img class="dark:hidden" /> + <img class="hidden dark:block" /> — это работает без JS и не вызывает FOUC. Два HTTP-запроса на логотип не идут — Astro инлайнит SVG лого в HTML.

Итог: миграция заняла 2 дня по факту, хотя рассчитывали на 4 часа. Зато убрали лишний JS-конфиг и получили нативную скорость Vite — сборка всего каталога проходит за полминуты.


Schema.org и AI-SEO: зачем тратить время

Каждая страница сайта генерирует JSON-LD структурированные данные:

  • Organization + WebSite — на всех страницах

  • BreadcrumbList — навигационная цепочка

  • ItemList — списки провайдеров и категорий

  • FAQPage — блоки вопрос-ответ (AnswerUnit)

  • BlogPosting — для статей блога

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [{
    "@type": "Question",
    "name": "Какой VPS выбрать для WordPress?",
    "acceptedAnswer": {
      "@type": "Answer",
      "text": "Для WordPress подойдёт VPS с 2+ GB RAM..."
    }
  }]
}
</script>

Плюс мы добавили llms.txt и llms-full.txt — краткое и полное описание сайта для AI-краулеров (GPTBot, PerplexityBot, ClaudeBot). В robots.txt разрешены все основные боты. Это не классический SEO, а GEO (Generative Engine Optimization) — оптимизация под ответы AI-ассистентов.


Partytown: как мы вынесли 1 190 ms TBT в Web Worker

Это главная история проекта. На старте всё было прозаично: GTM, GA4 (через GTM-контейнер), Яндекс.Метрика с Вебвизором, Microsoft Clarity. Все скрипты грузились лениво — после первого user-interaction. Считали, что этого достаточно.

Lighthouse на mobile в режиме throttled показал другое. Performance score — 76. TBT — 1 190 ms. Reduce JS execution time — 1,5 секунды. Reduce unused JavaScript — 163 KiB. Картина грустная.

Откуда такие цифры? Lighthouse симулирует не только медленную сеть, но и slow CPU 4x — и рандомно триггерит user events вроде mousemove. Наш «ленивый» загрузчик немедленно реагировал и грузил GTM прямо во время аудита. Итог: 1 516 ms script evaluation на main thread из-за GTM, Метрики и Clarity вместе. При этом CrUX (реальные данные пользователей Chrome) показывал 100. В продакшене всё было хорошо. А лаб-аудит для PageSpeed Insights врал безбожно.

Полумеры мы перепробовали:

  • Убрали mousemove из триггеров — Lighthouse его симулирует автоматически. Помогло на 5-10 пунктов.

  • Увеличили idle timeout с 3 до 10 секунд — Lighthouse-аудит длится 6-9 с, GTM не успевал. Снова +5 пунктов, но variance ±10 пунктов между запусками.

  • Обсуждали server-side GTM через свой домен (sgtm.example.com) — но это отдельный Docker-контейнер, лишняя инфраструктура и лишние расходы.

Финальное решение — @astrojs/partytown. Это Astro-обёртка над Partytown от Builder.io, которая переносит сторонние скрипты в Web Worker. Main thread остаётся чистым, аналитика работает в фоне.

Подключение в astro.config.mjs:

import partytown from '@astrojs/partytown';

export default defineConfig({
  integrations: [
    partytown({
      config: {
        forward: [
          'dataLayer.push',  // GTM
          'gtag',            // GA4
          'ym',              // Яндекс.Метрика
        ],
        debug: false,
      },
    }),
  ],
});

forward — список глобальных функций, которые проксируются с main thread в worker. Каждый вызов gtag('event', ...) сериализуется и отправляется в worker через MessageChannel.

Дальше делаем GTM-скрипт partytown-совместимым. В компоненте GoogleTagManager.astro:

<!-- Inline на main thread: dataLayer + Consent Mode v2 default = denied -->
<script is:inline>
  window.dataLayer = window.dataLayer || [];
  window.gtag = function () { window.dataLayer.push(arguments); };
  window.gtag('consent', 'default', { ad_storage: 'denied', /* ... */ });
  window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
</script>

<!-- Lazy loader: GTM грузится только по user interaction или 8 с idle.
     Partytown подхватывает type="text/partytown" и переносит в worker. -->
<script is:inline>
  function loadGTM() {
    if (window.__gtmLoaded) return;
    window.__gtmLoaded = true;
    var s = document.createElement('script');
    s.type = 'text/partytown';
    s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX';
    document.head.appendChild(s);
  }
  ['scroll', 'click', 'keydown', 'touchstart'].forEach(ev =>
    window.addEventListener(ev, loadGTM, { once: true, passive: true })
  );
  if ('requestIdleCallback' in window) {
    requestIdleCallback(loadGTM, { timeout: 8000 });
  } else {
    setTimeout(loadGTM, 8000);
  }
</script>

Аналогично для Яндекс.Метрики и Clarity — внутри их init-функции добавили script.type = 'text/partytown' перед script.src.

Что чуть не сломалось

Content Security Policy. Partytown создаёт Web Worker через Blob URL и выполняет скрипты через fetch() и eval(). Это требует трёх правок в CSP:

add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval' ... ;
  worker-src 'self' blob:;
  child-src 'self' blob:;
  connect-src 'self' https://www.googletagmanager.com https://cdn.jsdelivr.net ... ;
" always;

worker-src 'self' blob: — без этого worker не стартует. cdn.jsdelivr.net — потому что наш GTM-контейнер использует сторонний GTM-шаблон для Яндекс.Метрики, который динамически подгружает вспомогательный JS с jsdelivr. Без него в консоли висели CSP-violations и Best Practices падал до 96. Дебажили это через DevTools console — ошибки приходили из blob://, не из основного контекста, что путало.

Microsoft Clarity и forward. Сначала добавили 'clarity' в forward — получили TypeError: Cannot read properties of undefined (reading 'apply') в worker. Clarity внутри использует window.parent и свою очередь, которая конфликтует с partytown-проксями. Убрали clarity из forward — Clarity отлично работает в worker через свой <script type="text/partytown">, просто без main-thread прокси для вызовов извне (нам и не нужны).

Что получили

Слайд «до и после» по mobile throttled. Performance: было 76, стало 99. TBT: было 1 190 ms, стало 10 ms. Script evaluation на main thread: было 1 516 ms, стало около 50 ms. Best Practices и реальные пользователи в Chrome UX Report и были 100, и остались.

163 KiB GTM-кода больше не висят в main thread bundle. Они выполняются в worker и не блокируют рендер. При этом всё работает как раньше: партнёрская аналитика, Вебвизор с записью сессий, хитмапы Clarity. Просто на отдельном потоке.

Стабильные 100/100 на mobile throttled у нас не получились — Lighthouse даёт variance ±5 пунктов, потому что иногда симулирует interaction в первые две секунды (тогда GTM грузится в worker и съедает 100–150 ms TBT на setup). Реальные пользователи и CrUX этого не видят. Решили остановиться на 99: выжимать ещё пункт через server-side GTM нерентабельно.


Core Web Vitals: итоговые цифры

Сводно по Lighthouse: на десктопе 100 / 100 / 100 / 100, на мобильном с throttling — 99 / 100 / 100 / 100. Разбивка по Core Web Vitals на mobile throttled: LCP 1,7 секунды, FCP 1,4, CLS 0, TBT 10 ms, Speed Index 1,4. Без throttling, как в лаб-прогоне desktop: LCP 0,5 секунды, TBT 0 ms, CLS 0. TTFB по RUM — около 80 ms для российского региона и 50 ms для Европы. CDN не используем.

Что это дало в сумме:

  • Статическая генерация плюс Nginx с gzip и brotli.

  • Сторонний JS в Web Worker через Partytown.

  • inlineStylesheets: 'always' — весь CSS в HTML, ни одного render-blocking запроса.

  • Картинки через <Image /> Astro с Sharp в WebP и AVIF.

  • prefetch для внутренних ссылок (стратегия viewport).

  • Минимальный JS на основном потоке: квиз, мобильное меню и переключатель темы.

  • Полный набор security headers: HSTS, CSP с worker-src, X-Frame-Options, COOP, COEP, Referrer-Policy.


Что пошло не так: честный блок

  1. Strapi v5 Document Service. Миграция с v4 потребовала переписать все запросы. strapi.documents() не поддерживает relations (connect/set), поэтому для связей пришлось использовать REST API. Для плагинов — отдельная история: ни один плагин не имел стабильной версии под v5, пришлось форкать и собирать из main-веток.

  2. Draft & Publish дублирование. Strapi v5 хранит draft и published как отдельные записи в PostgreSQL. При обновлении контента через API нужно синхронизировать обе версии, иначе данные расходятся. Столкнулись, когда изменённая цена провайдера появлялась в admin-UI, но API отдавал старую draft. Решили через фильтр ?status=published в сборочных фетчах.

  3. Пустые каталожные страницы. При очередном пересмотре каталога часть категорий услуг оставалась без карточек — страницы показывали пустой листинг. Для Google это thin content. Добавили <meta name="robots" content="noindex"> для пустых каталогов, исключили их из sitemap.xml и из внутренних линков.

  4. Мобильное меню. Header с position: fixed и большим количеством пунктов в мобильном меню уходил за viewport без скролла на iPhone SE. Потребовалось max-height: calc(100dvh - 6rem) + overflow-y: auto + блокировка скролла страницы при открытом меню (document.body.style.overflow = 'hidden'). 100dvh вместо 100vh — потому что Safari iOS считает высоту въезжающего address bar отдельно и 100vh выходил за экран на 60 пикселей.

  5. Affiliate-ссылки. Большая часть ссылок в старых статьях содержала прямые URL без реферальных параметров. Мы не стали править данные в Strapi пакетно (каждая статья уникальна, mass-update опасен для SEO), а сделали обёртку resolveAffiliateUrl() — на этапе рендеринга любой внешний URL заменяется на реферальный, добавляются UTM и ставится rel="sponsored nofollow" (атрибут sponsored Google ввёл в 2019 году). Двойная защита: даже если в CMS введена неправильная ссылка, на сайте будет правильная.

  6. Cookie Consent + Consent Mode v2. Российский ФЗ «О персональных данных» + GDPR требуют явного согласия на analytics-cookies. Баннер с 4 категориями (necessary, analytics, advertising, functional), Google Consent Mode v2 c дефолтным значением denied, персист в localStorage, применение согласия без релоада страницы. Чтобы GTM реагировал в worker на update согласия, gtag в partytown forward — обязательно.


Инфраструктура и деплой

Чтобы картина была полной — стек в проде:

  • Сервер: 1 VPS на Ubuntu LTS, небольшая конфигурация (статике не нужны ресурсы в runtime)

  • Web-сервер: Nginx с brotli и gzip, HSTS preload, OCSP stapling, security headers вынесены в отдельный конфиг-файл

  • CMS: Strapi 5.x под PM2, PostgreSQL, cluster mode с auto-restart

  • Build pipeline: скрипт делает fetch из Strapi, astro build, atomic-подмену dist/ в prod-папку под nginx, пинг в IndexNow

  • Sitemap: custom на TypeScript (вместо @astrojs/sitemap) — нужен был per-page priority/changefreq/lastmod и noindex для пустых каталогов

Сборка всех страниц — около 30 секунд, деплой без downtime, один недорогой VPS спокойно держит весь трафик.


Astro 5 → 6: миграция в мае 2026 без downtime

Astro 6.0 вышел в апреле 2026 и принёс две вещи, ради которых мы решились на обновление: Vite 7 (быстрее dev и build, лучше tree-shaking) и обновлённый Image Service с lazy-decoded WebP/AVIF. Ключевых breaking changes в SSG-режиме почти нет — основные поломки в экосистеме плагинов.

Что сломалось:

  • lucide-astro помечен как deprecated, официальный пакет теперь @lucide/astro. Массовый реплейс во всех компонентах — from 'lucide-astro'from '@lucide/astro'. Иконки и их props идентичны, правок в коде отрисовки не требуется.

  • Vitest 4.1+ тянет Vite 8 через transitive deps, а Tailwind 4.3 + @tailwindcss/vite не совместимы с Vite 8 на дату обновления. Решили через overrides в package.json — форсили Vite 7.3.3, Vitest зафиксировали на 4.0.5. Из всех ожидаемых вещей эта съела больше всего времени: npm резолвит зависимости из нескольких поддеревьев с разными требованиями к Vite, и без явного overrides lock-файл регулярно подтягивал несовместимую версию.

  • Astro Image Service. ProviderImage-обёртки работают как раньше, но сборка стала быстрее примерно на 30%: 32 секунды вместо 45 для всего каталога.

Итоговый стек после миграции: Astro 6.3.7 + Vite 7.3.3 + Tailwind 4.3.0 + Strapi 5.42.1 + Node 22.22.1.

Сам деплой — zero-downtime atomic swap. На сервере лежит предыдущая dist/, параллельно собирается новая в dist-new/, потом два mv и nginx -s reload. Время недоступности — меньше секунды. Поле CrUX после обновления не просело, пользователи ничего не заметили.

Главный урок, который вбили во внутренние регламенты: никогда не запускать npm run build напрямую в prod-папке. Один раз из привычки запустили — пять минут 5xx, пока сборка в живом dist/ писала файлы. Теперь все ребилды идут только через один shell-скрипт с atomic swap, остальные пути перекрыты на уровне прав.


Когда этот стек имеет смысл

Astro + Strapi + Tailwind 4 подходит, если:

  • Контентный сайт от 50 страниц: блог, каталог, обзоры, landing-ферма.

  • Контент обновляется раз в день-неделю, не в реальном времени.

  • Core Web Vitals вы воспринимаете всерьёз и вам нужны 95+ по всем категориям Lighthouse.

  • Нужна гибкость CMS для редакторов, но скорость статики для посетителей.

  • Бюджет ограничен. Один VPS вытащит сотни тысяч хитов в месяц.

Не подходит, если:

  • Нужна динамика в реальном времени: чаты, лив-обновления цен, бронирование.

  • Пользовательский контент с частыми правками. Тут лучше Next.js с ISR или SvelteKit.

  • SSR-критичные фичи: A/B-тесты на сервере, персонализация по гео- и пользовательским сегментам.

  • Команда из React-разработчиков, которым не хочется привыкать к синтаксису .astro файлов.

Главный вывод, ради которого эта статья. В 2026 году можно сделать быстрый сайт даже с полным набором маркетинговых скриптов. GTM, GA4, Вебвизор, Clarity — всё работает параллельно основной странице в Web Worker и не влияет на метрики. Маркетологи счастливы, пользователь не видит торможений, Google видит высокие CrUX. Все выиграли.


Что впереди

План на ближайшие месяцы:

  • Расширение контентной базы. Автоматизация редакторского процесса в admin-панели — подсказки по SEO-meta, structured data и полноте заполнения полей.

  • Server-Side GTM. Оставили на всякий случай — если появятся жёсткие требования 100/100 на mobile throttled, перенесём GTM на собственный поддомен через Docker. Пока это overkill.


Ссылка на проект

Рабочий пример всего описанного — easylinklife.com. Код не открыт: внутри живёт логика работы с партнёрами и партнёрские договоры. Но архитектура и подходы воспроизводимы. Если собираете похожий проект — задавайте вопросы в комментариях, отвечу по технической части. Подводных камней было больше, чем поместилось в одну статью.