<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Baranov.Guru | RSS feed</title>
        <link>https://baranov.guru</link>
        <description>IT-консалтинг и внедрение ИИ: сайты и веб-приложения, автоматизация процессов, Telegram-боты и Mini Apps, бэкенд и облачная инфраструктура. Практические материалы по разработке, интеграции AI и архитектуре стартапов. Помогаем бизнесу снижать издержки, ускорять операции и повышать конверсию — более 15 лет опыта.</description>
        <lastBuildDate>Thu, 14 May 2026 06:52:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Baranov.Guru RSS feed</generator>
        <language>ru</language>
        <image>
            <title>Baranov.Guru | RSS feed</title>
            <url>https://baranov.guru/assets/og.webp</url>
            <link>https://baranov.guru</link>
        </image>
        <copyright>All rights reserved 2026, Baranov.Guru</copyright>
        <item>
            <title><![CDATA[10 ошибок, из-за которых сайт не приносит заявки]]></title>
            <link>https://baranov.guru/posts/website-no-leads-mistakes/</link>
            <guid>https://baranov.guru/posts/website-no-leads-mistakes</guid>
            <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Разбираем 10 распространённых ошибок, из-за которых сайт не конвертирует посетителей в клиентов и не приносит заявки.]]></description>
            <content:encoded><![CDATA[<div><blockquote><p>Почему сайт получает трафик, но не конвертирует посетителей в клиентов?</p></blockquote></div><p>У бизнеса может быть <a href="https://baranov.guru/services/website/">современный сайт</a>, настроенная реклама и <a href="https://baranov.guru/services/technical-seo/">стабильный поисковый трафик</a>. Но при этом заявок — единицы. Или стоимость лида оказывается слишком высокой.</p><p>В большинстве случаев проблема не связана с одной критической ошибкой. Обычно сайт просто не помогает пользователю принять решение и пройти путь до обращения.</p><p>Ниже — 10 распространённых причин, из-за которых сайты теряют потенциальных клиентов.</p><div><section><h3 id="error-1">Пользователь не понимает, чем занимается компания</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/01.webp" alt="Пользователь не понимает, чем занимается компания"></figure><p>Первые секунды на сайте определяют, останется человек или закроет вкладку.</p><p>Пользователь должен <strong>быстро</strong> получить ответы на три вопроса:</p><ul><li><strong>Что это за компания?</strong></li><li><strong>Какие услуги или продукты здесь предлагают?</strong></li><li><strong>Подходит ли это под мою задачу?</strong></li></ul><p>Если сайт не даёт понятного ответа — посетитель уходит.</p><p>Частая ошибка — <strong>абстрактные формулировки</strong> на первом экране:</p><ul><li>«Комплексные digital-решения»</li><li>«Инновационный подход к развитию бизнеса»</li><li>«Создаём будущее вместе»</li></ul><p>Такие фразы выглядят универсально, но <strong>не объясняют ценность</strong>.</p><p>Намного эффективнее работают конкретные формулировки:</p><ul><li>«Разработка интернет-магазинов на Tilda под ключ»</li><li>«Настройка рекламы в Яндекс Директ для стоматологий»</li><li>«Производство кухонь на заказ в Москве за 30 дней»</li></ul><p><strong>Чем понятнее</strong> предложение, тем <strong>выше вероятность</strong>, что пользователь продолжит изучать сайт.</p></section><section><h3 id="error-2">Интерфейс перегружен лишними элементами</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/02.webp" alt="Интерфейс перегружен лишними элементами"></figure><p><strong>Избыточный дизайн</strong> часто <strong>снижает конверсию</strong>.</p><p>Когда на странице одновременно используются:</p><ul><li>анимации;</li><li>слайдеры;</li><li>всплывающие окна;</li><li>видео;</li><li>автозапуск чатов;</li><li>сложные визуальные эффекты —</li></ul><p>внимание пользователя рассеивается.</p><p>Вместо того чтобы вести посетителя к заявке, <strong>сайт начинает отвлекать</strong>.</p><p>Эффективный продающий сайт строится вокруг понятного сценария:</p><ol><li><strong>проблема;</strong></li><li><strong>решение;</strong></li><li><strong>преимущества;</strong></li><li><strong>кейсы;</strong></li><li><strong>подтверждение экспертизы;</strong></li><li><strong>заявка.</strong></li></ol><p><strong>Чем меньше</strong> визуального шума — <strong>тем проще</strong> пользователю принять решение.</p></section><section><h3 id="error-3">На сайте нет сильного оффера</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/03.webp" alt="На сайте нет сильного оффера"></figure><p>Даже качественный дизайн не компенсирует отсутствие понятного предложения.</p><p>После просмотра страницы пользователь должен понимать <strong>почему стоит обратиться именно в эту компанию</strong>.</p><p>Слабый оффер выглядит так:</p><blockquote><p>Разработка сайтов для бизнеса</p></blockquote><p>Более сильный вариант:</p><blockquote><p>Разрабатываем сайты для B2B-компаний, которые помогают получать заявки из SEO и рекламы</p></blockquote><p>Или:</p><blockquote><p>Запускаем интернет-магазины на Tilda за 21 день с интеграцией оплаты и доставки</p></blockquote><p>Хороший оффер содержит:</p><ul><li><strong>конкретику;</strong></li><li><strong>понятную выгоду;</strong></li><li><strong>специализацию;</strong></li><li><strong>результат;</strong></li><li><strong>сроки или ограничения.</strong></li></ul><p><strong>Чем меньше</strong> пользователю <strong>приходится додумывать</strong> ценность самостоятельно, <strong>тем выше конверсия</strong>.</p></section><section><h3 id="error-4">Сайт медленно загружается</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/04.webp" alt="Сайт медленно загружается"></figure><p><strong>Скорость напрямую влияет на количество заявок.</strong></p><p>Если страница загружается 5–7 секунд, часть пользователей <strong>не дождётся</strong> открытия сайта — особенно с мобильных устройств.</p><p>Типовые причины:</p><ul><li>тяжёлые изображения;</li><li>перегруженный frontend;</li><li>большое количество сторонних скриптов;</li><li>слабый хостинг;</li><li>избыточные анимации.</li></ul><p>Медленный сайт влияет не только на удобство пользователя, но и на:</p><ul><li>конверсию;</li><li>стоимость рекламы;</li><li>SEO-показатели;</li><li>глубину просмотра;</li><li>поведенческие факторы.</li></ul><p><strong>Чем медленнее работает сайт, тем выше потери рекламного бюджета.</strong></p></section><section><h3 id="error-5">Мобильная версия неудобна</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/05.webp" alt="Мобильная версия неудобна"></figure><p>Во многих нишах мобильный трафик уже <strong>превышает 70%</strong>.</p><p>При этом часть сайтов по-прежнему проектируется преимущественно под десктоп.</p><p>В результате на смартфоне:</p><ul><li>текст слишком мелкий;</li><li>кнопки неудобны для нажатия;</li><li>формы работают некорректно;</li><li>элементы интерфейса ломаются;</li><li>страницы долго загружаются.</li></ul><p>Пользователь <strong>не будет адаптироваться</strong> под неудобный интерфейс. Он просто <strong>уйдёт к конкурентам</strong>.</p><p>Проверить качество мобильной версии можно очень просто: открыть сайт с телефона и попробовать оставить заявку одной рукой.</p><p>Большинство проблем становятся очевидны сразу.</p></section></div><aside><h3>Сайт не приносит заявки?</h3><p>Устраним технические проблемы, которые мешают сайту приносить деньги: медленная загрузка, плохая мобильная версия, непонятный оффер и проблемы с SEO.</p></aside><div><section><h3 id="error-6">Сайт не вызывает доверия</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/06.webp" alt="Сайт не вызывает доверия"></figure><p>Пользователь редко оставляет заявку компании, о которой невозможно получить базовую информацию.</p><p>Особенно в сегментах с высоким чеком.</p><p>Если на сайте отсутствуют:</p><ul><li>кейсы;</li><li>отзывы;</li><li>примеры работ;</li><li>информация о компании;</li><li>реальные фотографии;</li><li>юридические данные;</li><li>понятные контакты —</li></ul><p>доверие к бизнесу снижается.</p><p>Даже если компания работает давно и качественно.</p><p>Сильнее всего доверие повышают:</p><ul><li><strong>реальные цифры;</strong></li><li><strong>конкретные результаты;</strong></li><li><strong>фотографии проектов;</strong></li><li><strong>видео;</strong></li><li><strong>разборы кейсов;</strong></li><li><strong>упоминания клиентов и партнёров.</strong></li></ul><p><strong>Блоки доверия</strong> — это не второстепенный контент, а один из <strong>ключевых факторов</strong> конверсии.</p></section><section><h3 id="error-7">Слишком сложные формы заявки</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/07.webp" alt="Слишком сложные формы заявки"></figure><p>Чем <strong>больше полей</strong> содержит форма — тем <strong>ниже вероятность</strong>, что пользователь её заполнит.</p><p>Типичная ошибка — попытка собрать максимум информации на первом контакте:</p><ul><li>имя;</li><li>телефон;</li><li>email;</li><li>компания;</li><li>бюджет;</li><li>комментарий;</li><li>количество сотрудников;</li><li>удобное время звонка.</li></ul><p>Для большинства сайтов это <strong>избыточно</strong>.</p><p>Основная задача первой формы — не собрать подробную анкету, а <strong>начать коммуникацию</strong>.</p><p>Во многих случаях достаточно:</p><ul><li>имени;</li><li>телефона или Telegram.</li></ul><p>Остальные детали можно уточнить позже.</p></section><section><h3 id="error-8">Нет понятного призыва к действию</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/08.webp" alt="Нет понятного призыва к действию"></figure><p>Иногда пользователь готов обратиться, но сайт не даёт <strong>понятного</strong> следующего шага.</p><p>Типовые проблемы:</p><ul><li>кнопки плохо заметны;</li><li>форма находится слишком низко;</li><li>CTA не объясняет выгоду;</li><li>непонятно, что произойдёт после отправки заявки.</li></ul><p>Слабые CTA:</p><blockquote><p>Отправить</p></blockquote><blockquote><p>Подробнее</p></blockquote><p>Более эффективные варианты:</p><ul><li>«Получить расчёт стоимости»</li><li>«Обсудить проект»</li><li>«Получить аудит сайта»</li><li>«Узнать сроки запуска»</li></ul><p>Хороший призыв к действию всегда:</p><ul><li><strong>конкретен;</strong></li><li><strong>понятен;</strong></li><li><strong>связан с ценностью для клиента.</strong></li></ul></section><section><h3 id="error-9">Сайт создаётся «для себя», а не для клиента</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/09.webp" alt="Сайт создаётся «для себя», а не для клиента"></figure><p>Многие компании пытаются <a href="https://baranov.guru/services/website/">сделать сайт</a> максимально необычным:</p><ul><li>сложный дизайн;</li><li>нестандартную навигацию;</li><li>большое количество анимаций;</li><li>креативные интерфейсные решения.</li></ul><p>Но задача пользователя — не оценивать дизайнерские эксперименты.</p><p>Он хочет быстро решить свою задачу.</p><p>Если интерфейс мешает получить информацию или оставить заявку, сайт начинает снижать конверсию.</p><p>На практике <strong>самые эффективные</strong> коммерческие сайты часто выглядят достаточно <strong>сдержанно</strong>.</p><p>Потому что их <strong>основная цель</strong> — не производить впечатление, а <strong>приводить клиентов</strong>.</p></section><section><h3 id="error-10">Нет аналитики и данных</h3><figure><img src="https://baranov.guru/assets/posts/website-no-leads-mistakes/10.webp" alt="Нет аналитики и данных"></figure><p>Без <a href="https://baranov.guru/services/web-analytics/">аналитики</a> <strong>невозможно понять</strong>, почему сайт не приносит заявки.</p><p>Если не настроено отслеживание действий пользователей, бизнес не видит:</p><ul><li>где теряется трафик;</li><li>какие страницы работают хуже;</li><li>какие источники приводят клиентов;</li><li>сколько стоит лид;</li><li>какие каналы окупаются.</li></ul><p>В результате любые изменения превращаются <strong>в предположения</strong>.</p><p>Базовый набор аналитики должен включать:</p><ul><li>Яндекс Метрику;</li><li>Google Analytics;</li><li>настройку целей;</li><li>отслеживание форм;</li><li>коллтрекинг при необходимости;</li><li>аналитику рекламных кампаний.</li></ul><p>Только <strong>данные позволяют системно повышать конверсию сайта</strong>.</p></section></div><h2>Вывод</h2><p>Проблема большинства сайтов не в дизайне. Чаще всего сайт просто <strong>не помогает</strong> пользователю <strong>принять решение и сделать следующий шаг</strong>.</p><p>На конверсию влияют:</p><ul><li>понятность предложения;</li><li>скорость работы;</li><li>мобильная версия;</li><li>доверие;</li><li>структура страницы;</li><li>качество оффера;</li><li>сценарий взаимодействия.</li></ul><p><strong>Эффективный сайт</strong> — это не набор визуальных эффектов, а <strong>инструмент продаж</strong>.</p><p>И <strong>чем проще</strong> пользователю:</p><ul><li>понять предложение;</li><li>оценить выгоду;</li><li>убедиться в надёжности компании;</li><li>оставить заявку —</li></ul><p><strong>тем выше</strong> вероятность <strong>конверсии</strong>.</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/website-no-leads-mistakes/og.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Настройка MAX-бота для работы в Yandex Cloud Functions]]></title>
            <link>https://baranov.guru/posts/yandex-cloud-functions-max-bot/</link>
            <guid>https://baranov.guru/posts/yandex-cloud-functions-max-bot</guid>
            <pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по настройке и хостингу простейшего MAX-бота в serverless-окружении Yandex Cloud Functions.]]></description>
            <content:encoded><![CDATA[<h2 id="why">Зачем нужна эта статья? </h2><p>MAX-боты умееют получать события от платформы несколькими способами:</p><ul><li>В режиме <strong>ожидания запросов</strong> от сервера (long-polling);</li><li>В режиме получения событий через <strong>webhook</strong>;</li></ul><p>В <a href="https://dev.max.ru/docs/chatbots/bots-coding/prepare">документации</a> к официальной <a href="https://dev.max.ru/docs/chatbots/bots-coding/library/js">typescript библиотеке</a> для создания MAX-ботов есть примеры запуска ботов в режиме ожидания запросов. В котором бот подключается к серверам MAX и ждёт события обновления. Как только сервер присылает обновление, бот обрабатывает его.</p><p>К сожалению такой режим работы не подходит для <strong>serverless</strong> окружений, а примеров запуска бота через получение событий по вебхуку, на момент написания этой статьи, в документации не нашлось.</p><p>В этой статье я постараемся исправить этот недочёт.</p><h2 id="solution">Решение для тех кто торопится </h2><p>Для тех кто спешит и умеет настраивать бота для работы с webhook, сразу публикуем базовое готовое решение:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Update } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '@maxhub/max-bot-api/dist/core/network/api'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Bot, Context } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '@maxhub/max-bot-api'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">event</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexCloudFunctionEvent</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (event.body) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">typeof</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> event.body </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'string'</span><span style="color:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> event.body.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#D73A49;--shiki-dark:#F97583"> ></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">      try</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">        // Парсим тело запроса</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> update</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Update</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> JSON</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(event.body) </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Update</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">        // Создаём бота</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> bot</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createBot</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">        // Ниже то, для чего нужна эта статья</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ctx</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Context</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(update, bot.api, bot.botInfo);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> bot.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">middleware</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()(ctx, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { statusCode: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">200</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, body: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'ok'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="color:#D73A49;--shiki-dark:#F97583">catch</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { statusCode: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">500</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, body: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'error'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { statusCode: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">200</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, body: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'ok'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createBot</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // В данном примере токен для бота хранится в переменной окружения</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> token</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">MAX_BOT_TOKEN</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">token) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    throw</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'MAX_BOT_TOKEN не найден'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> bot</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Bot</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(token);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // Тут описываете вашу логику работы бота</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> bot;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><aside><h3>Нужен MAX-бот под ваш сценарий?</h3><p>Сделаем MAX-бота “под ключ”: архитектура, хостинг, webhook, логика, тестирование и поддержка.</p></aside><h2 id="explanation">Разбор решения </h2><p>Не буду долго и подробно расписывать как мы его нашли, но тем не менее отмечу пару важных моментов:</p><p>Мы искали что-то похожее на</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">bot.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">handleUpdate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(update)</span></span></code></pre><p>из <a href="https://grammy.dev/">grammY</a>, библиотеки для Telegram-ботов.</p><p>И действительно, такой метод у класса Bot есть, но он приватный и не доступен для вызова.</p><p>Тут наверное можно было бы кастануть Bot к нужному мне типу с не приватным <strong>handleUpdate</strong>, но мы решили чуть-чуть покопать и изучить код библиотеки.</p><p>В итоге поняли что можно вызвать <strong>middleware()</strong>, предварительно добавив событие Update в контекст бота.</p><h2 id="webhook">Подписка функции на получение событий </h2><p>Для того чтобы подписать облачную функцию на обработку событий нужно:</p><ul><li><strong>Знать публичный url для вызова функции</strong> (можно посмотреть в настройках функции в консоли управления Yandex Cloud);</li><li><strong>Отправить</strong> <a href="https://dev.max.ru/docs-api/methods/POST/subscriptions"><strong>API запрос</strong></a></li></ul><p>На этом всё. <strong>Спасибо за внимание!</strong></p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/yandex-cloud-functions-max-bot/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Автоматизируем публикацию npm-пакета с помощью Github Actions]]></title>
            <link>https://baranov.guru/posts/github-actions-npm-publish/</link>
            <guid>https://baranov.guru/posts/github-actions-npm-publish</guid>
            <pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по подготовке npm-пакета к автоматизированному релизу с помощью Github Actions.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ В ноябре 2025 года, npm изменили свою политику использования <strong>access tokens</strong>. <a href="https://docs.npmjs.com/about-access-tokens">Подробнее</a>.</p><p>Поэтому часть статьи про <a href="#token">получение токена</a> устарела.</p></aside><p>Не так давно мы <a href="https://baranov.guru/posts/react-telegram-widgets">выпустили npm-пакет @baranov-guru/react-telegram-widgets</a>. В этой статье решили поделиться своим опытом по автоматизации сборки и публикации обновлений npm-пакетов.</p><p>В начале статьи дана небольшая справка, о том что такое <strong>NPM</strong> и <strong>Github Actions</strong>.</p><aside class="post-disclaimer" role="note"><p>💡 Если вам интересно сразу готовое решение, <a href="#result">жмите сюда</a>.</p></aside><h2 id="npm">Что такое NPM? </h2><p><strong>NPM</strong> (Node Package Manager) — это менеджер пакетов для <strong>JavaScript</strong>. Проще говоря - это программа, которая помогает устанавливать и управлять чужими библиотеками (частями кода), и использовать их в своём JavaScript-проекте.</p><p>Она идёт в комплекте с <strong>Node.js</strong> и позволяет:</p><ul><li>быстро <strong>подключать нужные инструменты</strong> и функции,</li><li><strong>обновлять их</strong> до новых версий,</li><li><strong>удалять</strong>, если больше не нужны.</li></ul><p>Также через <strong>NPM</strong> можно найти тысячи готовых решений, которые другие разработчики выложили в общий доступ.</p><h2 id="github-actions">Что такое Github Actions? </h2><p><strong>GitHub Actions</strong> — это инструмент внутри <a href="https://github.com">GitHub</a>, который предоставляет инструменты для непрерывной интеграции и непрерывной поставки (<strong>CI/CD</strong>). Или если проще, то помогает автоматически выполнять задачи с вашим кодом:</p><ul><li><strong>собирать</strong> проект,</li><li>проверять, работает ли код (<strong>прогонять тесты</strong>),</li><li><strong>выкладывать</strong> его на сервер или в облако.</li></ul><p>Всё это происходит автоматически, по <strong>заранее созданным правилам</strong>, описанным в <strong>workflow</strong> файлах. Это удобно, потому что не нужно делать всё вручную.</p><h2 id="workflow">Собираем свой Github workflow </h2><p>Углубляться во все подробности написания workflow файлов, так же известных как "рабочие процессы", в этой статье не будем, оставим лишь <a href="https://docs.github.com/ru/actions/how-tos/writing-workflows">ссылку на официальную документацию</a>.</p><p>Нас интересует два момента:</p><ul><li><a href="#trigger"><strong>Когда запускать</strong> workflow?</a>;</li><li><a href="#publish"><strong>Как публиковать</strong> в npm</a>;</li></ul><h2 id="trigger">Когда запускать workflow? </h2><p>Для себя мы выбрали следующую схему работы:</p><ul><li>Все изменения вливаются в <strong>main ветку</strong> проекта;</li><li>Публикация же в npm происходит после ручного создания <a href="https://docs.github.com/ru/repositories/releasing-projects-on-github/about-releases">Github Release</a>.</li></ul><p>Для этого надо добавить следующий код в workflow:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  release</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    types</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">published</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre><p>Если же вы хотите публиковаться каждый раз при <strong>добавлении коммита в main</strong> ветку, то надо прописать следующее:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    branches</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre><h2 id="publish">Как публиковать в npm? </h2><p>Тут всё просто, необходимо использовать команду:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> publish</span></span></code></pre><p>Шаг в workflow может выглядеть следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">- </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Publish to npm</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">npm publish</span></span></code></pre><p>Однако, если запустить такой workflow, то он <strong>упадёт с ошибкой авторизации</strong>, при публикации пакета.</p><p>Причина банальна - <strong>нельзя</strong> опубликовать пакет <strong>анонимно</strong>, нужно представиться.</p><p>Для этого необходимо <strong>добавить токен</strong>.</p><h3 id="token">Получение токена </h3><p>Получить токен можно в личном кабинете <a href="https://www.npmjs.com/">NPM</a>.</p><p>Для этого надо:</p><ul><li><strong>перейти на страницу</strong> с токенами;</li></ul><p><img src="https://baranov.guru/assets/posts/github-actions-npm-publish/access-tokens.webp" alt="Переходим на страницу с токенами в личном кабинете" title="Переходим на страницу с токенами в личном кабинете"></p><ul><li>затем <strong>создать токен</strong>, нажав на кнопку "<strong>Generate New Token</strong>";</li></ul><p>Для простых целей, нам хватит <strong>классического legacy access token</strong>. Его и выберем:</p><p><img src="https://baranov.guru/assets/posts/github-actions-npm-publish/generate-new-classic-token-button.webp" alt="Кнопка Generate New Token" title="Кнопка Generate New Token"></p><ul><li><strong>заполнить форму</strong> и <strong>скопировать</strong> полученный токен;</li></ul><p>Так как мы собираемся публиковаться через <strong>Github Actions</strong>, то выбираем тип токена "<strong>Automation</strong>".</p><p><img src="https://baranov.guru/assets/posts/github-actions-npm-publish/create-classic-token.webp" alt="Создание токена" title="Создание токена"></p><p>Полученный токен необходимо добавить в <strong>repository secrets</strong> в репозитории проекта на github.</p><h3 id="secrets">Добавление токена в Secrets на Github </h3><p>Нам необходимо создать секрет <strong>NPM_TOKEN</strong>. Для этого необходимо:</p><ul><li>перейти в <strong>настройки репозитория</strong>;</li><li>развернуть вкладку <strong>Secrets And Variables</strong>;</li><li>выбрать <strong>Actions</strong>;</li></ul><p>На открывшейся странице нажать кнопку <strong>New Repository Secret</strong> и вставить в модальном окне полученный ранее токен</p><p><img src="https://baranov.guru/assets/posts/github-actions-npm-publish/github-secrets.webp" alt="Добавление токена в Secrets на Github" title="Добавление токена в Secrets на Github"></p><p>Теперь осталось только указать токен в переменной окружения <strong>NODE_AUTH_TOKEN</strong> в нашем workflow:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">- </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Publish to npm</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">npm publish</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  env</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    NODE_AUTH_TOKEN</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">${{ secrets.NPM_TOKEN }}</span></span></code></pre><h3>Особенности публикации @scoped пакетов</h3><p>Если у вас, как и у нас, <a href="https://docs.npmjs.com/about-scopes">scoped package</a>, то <strong>по умолчанию</strong> пакет <strong>не будет</strong> публично доступен.</p><p>Для решиния этой проблемы необходимо добавить параметр <strong>access</strong> со значением <strong>public</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> publish</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> --access</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> public</span></span></code></pre><p>Как не трудно догадаться это сделает пакет <strong>публичным</strong> и предоставит к нему <strong>открытый доступ</strong>.</p><h3 id="provenance">Происхождение публикуемых пакетов (Provenance) </h3><p>Так же, <strong>Github Actions</strong> из коробки позволяют создавать <strong>сведения о происхождении</strong> публикуемых пакетов (<strong>Provenance</strong>).</p><p>Provenance содержит подписанную информацию о сборке и публикации проекта, тем самым <strong>повышает безопасность</strong> цепочки поставок пакетов и просто классно смотрится. 😉</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> publish</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> --provenance</span></span></code></pre><p>Однако, если запустить такой workflow, то мы увидим следующую ошибку:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> error</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> code</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> EUSAGE</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> error</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Provenance</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> generation</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> in</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> GitHub</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> Actions</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> requires</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "write"</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> access</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> to</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> the</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "id-token"</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> permission</span></span></code></pre><p>Как и написано в ошибке, необходимо добавить в workflow пермиссию <strong>id-token</strong> со значением <strong>write</strong>. Выглядит она следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">permissions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  id-token</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">write</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  contents</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">read</span></span></code></pre><p>Теперь, если вы всё сделали правильно, в опубликоманном пакете можно будет увидеть следующую плашку:</p><p><img src="https://baranov.guru/assets/posts/github-actions-npm-publish/provenance.webp" alt="Provenance" title="Плашка с provenance в описании пакета"></p><h2 id="result">Итоговый workflow </h2><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-yaml"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Название workflow</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Publish Package</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Когда выполнять</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  release</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    types</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">published</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Работа с разрешениями</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">permissions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  id-token</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">write</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  contents</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">read</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D"># Описание job</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">jobs</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  build-and-publish</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    runs-on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">ubuntu-latest</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    steps</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # Получение актуального кода проекта</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Checkout code</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        uses</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">actions/checkout@v4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # Настройка окружения</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Setup Node.js</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        uses</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">actions/setup-node@v4</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        with</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          node-version</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"18"</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          registry-url</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://registry.npmjs.org"</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          cache</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"npm"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # Опционально: Установка зависимостей, линтинг, прогон тестов</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # Сборка пакета</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Build package</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">npm run build</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">      # Публикация пакета</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Publish to npm</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">npm publish --provenance --access public</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        env</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          NODE_AUTH_TOKEN</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">${{ secrets.NPM_TOKEN }}</span></span></code></pre>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/github-actions-npm-publish/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем Telegram Widgets в React-приложение]]></title>
            <link>https://baranov.guru/posts/react-telegram-widgets/</link>
            <guid>https://baranov.guru/posts/react-telegram-widgets</guid>
            <pubDate>Tue, 15 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению Telegram Widgets в React-приложение с помощью npm-пакета @baranov-guru/react-telegram-widgets.]]></description>
            <content:encoded><![CDATA[<p>В этой статье расскажем что такое <strong>Telegram Widgets</strong>, зачем они нужны и как максимально просто добавить их в своё <strong>React</strong> приложение.</p><h2 id="what">Что такое Telegram Widgets? </h2><p><a href="https://core.telegram.org/widgets">Telegram Widgets</a> - <strong>API от Telegram</strong>, которое позволяет быстро добавить на сайт <strong>кнопку "Поделиться"</strong>, встроить <strong>посты из публичных каналов или групп</strong>. С его помощью пользователи также могут <strong>авторизоваться через Telegram</strong> или <strong>просматривать обсуждения</strong> прямо на сайте.</p><h2 id="problem">Проблема с использованием в React приложенияx </h2><p>Дело в том что <strong>Telegram Widget</strong> - это <strong>script-тег</strong> вида:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  async</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  src</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://telegram.org/js/telegram-widget.js?22"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  data-telegram-post</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"baranov_guru/61"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  data-width</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"100%"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">>&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre><p>Он инициализируется и добавляет в родительский контейнер iframe, который рендерит контент с нужными параметрами.</p><p>Из-за этого с виджетами очень неудобно работать в декларативном стиле, присущем React-компонентам. Поэтому нам пришлось написать небольшую обёртку над скриптами, позволяющую:</p><ul><li><strong>Подписываться на события загрузки</strong>;</li><li><strong>Отслеживать ошибки</strong>;</li><li>В удобном декларативном стиле <strong>пробрасывать необходимые свойства виджетов</strong>;</li><li>Корректно <strong>обрабатывать изменения свойств и перерендеры</strong> компонентов;</li></ul><p>А так как официального React-пакета для виджетов нет и не предвиделось, поэтому мы решили оформить её в виде npm-пакета <a href="https://www.npmjs.com/package/@baranov-guru/react-telegram-widgets">@baranov-guru/react-telegram-widgets</a>.</p><h2 id="package">Что делает пакет @baranov-guru/react-telegram-widgets? </h2><p>Сейчас он поддерживает два типа Telegram-виджетов:</p><ul><li><strong>Комментарии</strong> (<code>TelegramDiscussionWidget</code>) — возможность встроить обсуждение постов прямо на сайте.</li><li><strong>Посты из ТГ-каналов</strong> (<code>TelegramPostWidget</code>) — удобный способ вставить отдельный пост из публичного Telegram-канала или группы.</li></ul><h2>Установка</h2><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @baranov-guru/react-telegram-widgets</span></span></code></pre><h2 id="examples">Примеры использования </h2><h3 id="discussion-widget">Виджет комментариев (TelegramDiscussionWidget) </h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { TelegramDiscussionWidget } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@baranov-guru/react-telegram-widgets"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> App</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Discussion&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">TelegramDiscussionWidget</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        discussion</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"baranov_guru"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        commentsLimit</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">10</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        height</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">400</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        color</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"#ff0000"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        colorful</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        dark</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        onLoad</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Comments loaded!"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        onError</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{(</span><span style="color:#E36209;--shiki-dark:#FFAB70">error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Failed to load comments:"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, error)}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Этот компонент можно встроить под статьёй или любым другим контентом. Все комментарии будут сохраняться в Telegram и синхронизироваться между пользователями.</p><p>Комментарии к посту доступны в Telegram.</p><h3 id="post-widget">Встраивание поста (TelegramPostWidget) </h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { TelegramPostWidget } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@baranov-guru/react-telegram-widgets"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> App</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">TelegramPostWidget</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    post</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"baranov_guru/61"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    userpic</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    color</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"#ff0000"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    onLoad</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Post loaded!"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    onError</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{(</span><span style="color:#E36209;--shiki-dark:#FFAB70">error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Failed to load post:"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, error)}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"w-full flex p-2"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre><p>Просто указываете <strong>название канала и ID поста</strong> — и он появляется на странице.</p><p><a href="https://t.me/baranov_guru/62">Открыть пост в Telegram</a></p><h2 id="doc">Документация </h2><p>Все доступные пропсы и примеры есть в <a href="https://www.npmjs.com/package/@baranov-guru/react-telegram-widgets">npm-профиле пакета</a>. Там всё кратко и по делу.</p><h2>Поддержите проект ⭐️</h2><p>Если вам пригодился этот пакет — будем рады, если поставите ему звёздочку на <a href="https://github.com/baranov-guru/react-telegram-widgets">GitHub</a>. Это поможет другим разработчикам найти его.</p><div><p><img src="https://baranov.guru/assets/posts/react-telegram-widgets/cover.webp" alt="@baranov-guru/react-telegram-widgets"></p><p><a href="https://github.com/baranov-guru/react-telegram-widgets">@baranov-guru/react-telegram-widgets</a> — React components for embedding Telegram posts and comments widgets in your web applications.</p></div>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/react-telegram-widgets/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Новые требования по работе с персональными данными]]></title>
            <link>https://baranov.guru/posts/personal-data-law-changes/</link>
            <guid>https://baranov.guru/posts/personal-data-law-changes</guid>
            <pubDate>Fri, 11 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Обзор новых требований к обработке персональных данных для владельцев сайтов и чат-ботов и чек-лист для самопроверки.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note">⚠️ Информация, представленная в статье, носит исключительно информационный характер и не является юридической консультацией. Автор не несёт ответственности за точность, актуальность или полноту предоставленных данных. Для получения профессиональной юридической помощи рекомендуется обратиться к квалифицированному юристу.</aside><p>С <strong>30 мая 2025 года</strong> в России вступили в силу новые требования по защите персональных данных (<strong>ПДн</strong>). Ужесточены штрафы, усилена уголовная ответственность и расширен список нарушений.</p><p>Эти изменения <strong>касаются всех</strong> — от <strong>владельцев интернет-магазинов</strong> до <strong>авторов Telegram-ботов</strong>.</p><hr><h2 id="review">🏛️ Обзор новых законов и штрафов </h2><p>Согласно последним поправкам в законы № 420‑ФЗ и № 421‑ФЗ:</p><ul><li><strong>Штрафы от 100 000 до 300 000 ₽</strong> — за отсутствие регистрации в Роскомнадзоре как оператора ПДн.</li><li><strong>Оборотные штрафы от 1 до 3 млн ₽</strong> — за утечку данных, даже если виноват подрядчик.</li><li><strong>Уголовная ответственность</strong> — в случае умышленного или массового нарушения (например, при продаже баз).</li><li><strong>Штрафы для юрлиц и ИП</strong> — до 5 млн ₽ за отсутствие согласия, нарушение хранения и несоблюдение срока обработки.</li></ul><hr><h2 id="who">🎯 Кто считается оператором персональных данных? </h2><p>Если вы:</p><ul><li>собираете заявки или комментарии через сайт;</li><li>принимаете оплату онлайн;</li><li>используете email- или мессенджер-рассылки;</li><li>подключили чат-бот или CRM;</li></ul><p>— то вы <strong>оператор персональных данных</strong> и обязаны соблюдать законодательство.</p><p>Это <strong>касается всех</strong> — от крупных компаний до ИП и самозанятых.</p><hr><h2 id="site-actions">🖥️ Что нужно сделать владельцам сайтов? </h2><h3>1. Зарегистрироваться в Роскомнадзоре</h3><p>Если ваш сайт собирает любые данные (имя, телефон, email, cookies и пр.) — вы обязаны <a href="https://pd.rkn.gov.ru/operators-registry/notification/form/">уведомить Роскомнадзор и зарегистрироваться как оператор ПДн</a>.</p><h3>2. Разместить политику конфиденциальности</h3><p>На сайте должна быть доступна и легко читаемая <strong>политика обработки персональных данных</strong>, содержащая:</p><ul><li>наименование и контакты оператора;</li><li>цели и способы обработки;</li><li>категории собираемых данных;</li><li>срок хранения;</li><li>права пользователей;</li><li>способы отзыва согласия.</li></ul><p>Политика размещается в футере и отображается при первом входе на сайт (например, в виде баннера).</p><h3>3. Запросить согласие перед сбором</h3><p>Перед тем как собирать любые данные:</p><ul><li>покажите текст согласия;</li><li>добавьте <strong>чекбокс или кнопку "Согласен"</strong>, без которой форма не отправляется;</li><li>фиксируйте дату, IP и содержание согласия.</li></ul><p>Согласие должно быть <strong>свободным, конкретным, информированным и осознанным</strong>.</p><h3>4. Уведомить о cookie и сторонних скриптах</h3><p>Если сайт использует cookies, аналитические сервисы (Google Analytics, Яндекс.Метрика) или сторонние пиксели (TikTok), вы обязаны:</p><ul><li>уведомлять пользователей при входе на сайт;</li><li>позволять отключать cookies;</li><li>давать выбор — принимать или отклонять их.</li></ul><hr><h2 id="bot-actions">🤖 Как собирать данные в чат-ботах </h2><p>Если вы используете чат-ботов (Telegram, WhatsApp, ВКонтакте, Viber, Web):</p><h3>1. Зарегистрируйтесь как оператор ПДн</h3><p>Даже если бот — только для получения заявок, это уже обработка персональных данных.</p><h3>2. Получите явное согласие</h3><p>Попросите пользователя нажать «Согласен» или ввести команду (например, <code>/agree</code>) перед отправкой имени или телефона.</p><h3>3. Разместите ссылку на политику</h3><p>Политика конфиденциальности должна быть доступна из бота — через кнопку или автоответ.</p><h3>4. Фиксируйте факт согласия</h3><p>Сохраняйте дату, время, ID пользователя и текст, который он подтвердил.</p><hr><h2 id="security">🔐 Общие требования по безопасности </h2><ul><li><strong>Собирайте только нужное</strong> — минимизируйте объем данных.</li><li><strong>Храните данные в зашифрованном виде</strong>.</li><li><strong>Ограничьте доступ</strong> — только для сотрудников, которым он необходим.</li><li><strong>Удаляйте устаревшие данные</strong> — в срок, указанный в политике.</li><li><strong>Заключайте договоры с подрядчиками</strong>, которые обрабатывают ПДн от вашего имени.</li></ul><hr><h2 id="checklist">📋 Чек-лист для владельцев сайтов и чат-ботов </h2><table><thead><tr><th>Действие</th><th>Обязательно?</th><th>Комментарий</th></tr></thead><tbody><tr><td><a href="https://pd.rkn.gov.ru/operators-registry/notification/form/">Регистрация в Роскомнадзоре</a></td><td>✅ Да</td><td>Даже если собираете только email</td></tr><tr><td>Политика конфиденциальности</td><td>✅ Да</td><td>На сайте, в футере, доступна по ссылке из бота</td></tr><tr><td>Согласие пользователя</td><td>✅ Да</td><td>До отправки формы или сообщения</td></tr><tr><td>Уведомление о cookie</td><td>✅ Да</td><td>При первом посещении сайта</td></tr><tr><td>Минимизация данных</td><td>✅ Да</td><td>Не запрашивайте лишнего</td></tr><tr><td>Хранение и защита</td><td>✅ Да</td><td>Пароли, шифрование, логирование доступа</td></tr><tr><td>Обработка через подрядчиков</td><td>⚠️ С осторожностью</td><td>Только по договору, с передачей ответственности</td></tr><tr><td>Трансграничная передача (например, Telegram)</td><td>⚠️ Доп. уведомление в РКН</td><td>Особенно, если сервера за пределами РФ</td></tr></tbody></table><hr><h2 id="summary">📌 Выводы </h2><ol><li><strong>Новые законы усилили контроль</strong> — штрафы стали выше, а ответственности больше.</li><li><strong>Каждый сайт и бот — это потенциальный обработчик данных</strong>.</li><li><strong>Главное правило — явное согласие</strong>: без него любые сборы ПДн незаконны.</li><li><strong>Автоматизация ≠ безответственность</strong> — даже если все делает бот, ответственность несет владелец.</li><li><strong>Правильная настройка сайта и чат-бота сегодня — защита от миллионов штрафов завтра.</strong></li></ol><p>На этом всё. Спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/personal-data-law-changes/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Брендированные типы (branded types) в TypeScript]]></title>
            <link>https://baranov.guru/posts/typescript-branded-types/</link>
            <guid>https://baranov.guru/posts/typescript-branded-types</guid>
            <pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Что такое branded types в Typescript и как с их помощью улучшить type safety в рантайме.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2025 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><h2 id="what">Что такое брендированные типы в TypeScript? </h2><p><strong>Брендированные типы</strong> — это паттерн в TypeScript, позволяющий повысить безопасность типов, добавляя <em>уникальные метки</em> к существующим типам (например, <code>string</code> или <code>number</code>).</p><p>Это особенно полезно, когда несколько значений имеют одинаковый базовый тип, но несут разную семантическую нагрузку — например, <code>UserId</code>, <code>PostId</code>, <code>OrderId</code>.</p><p>Несмотря на то что на уровне выполнения это всё те же строки или числа, компилятор TypeScript воспринимает их как разные типы. Это предотвращает случайную подстановку одного значения вместо другого.</p><h2>Зачем нужны брендированные типы?</h2><p>TypeScript обеспечивает <em>структурную типизацию</em> — тип считается корректным, если у него есть нужные поля, независимо от его имени. Это может привести к ошибкам, когда два значения совместимы по структуре, но не по смыслу. Например:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> User</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">ownerId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostByUser</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">userId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">postId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// вызов</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">getPostByUser</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.id, user.id) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// ❌ ошибка смысла, но TypeScript не ругается</span></span></code></pre><p>С брендированными типами TypeScript «поймёт» ошибку на этапе компиляции:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> &#x26;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">__brand</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'UserId'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PostId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> &#x26;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">__brand</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'PostId'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostByUser</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">userId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">postId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PostId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">getPostByUser</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.id, user.id) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// ❌ ошибка типов: PostId ≠ UserId</span></span></code></pre><h2 id="how">Как создать брендированный тип? </h2><p>Существует несколько подходов. Простейший — через пересечение типов с объектом-брендом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">K</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6F42C1;--shiki-dark:#B392F0">T</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> K</span><span style="color:#D73A49;--shiki-dark:#F97583"> &#x26;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#E36209;--shiki-dark:#FFAB70">__brand</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> T</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"UserId"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> ProductId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ProductId"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span></code></pre><p>Чтобы избежать дублирования имён и скрыть бренд от автодополнения в редакторе, можно использовать <code>unique symbol</code>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">declare</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> __brand</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> unique</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> symbol</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">T</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6F42C1;--shiki-dark:#B392F0">B</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> T</span><span style="color:#D73A49;--shiki-dark:#F97583"> &#x26;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { [__brand]</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> B</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"UserId"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span></code></pre><h2 id="pros">Преимущества брендированных типов </h2><ul><li>✅ <strong>Повышенная безопасность типов (type safety)</strong>: предотвращают подстановку несовместимых значений.</li><li>📖 <strong>Ясность кода</strong>: код становится самодокументируемым — <code>UserId</code>, <code>OrderId</code> говорят сами за себя, упрощают понимание, поддержку и рефакторинг кода</li><li>🧪 <strong>Простота тестирования и валидации</strong>: можно создавать отдельные функции-валидаторы, возвращающие брендированный тип только при успешной проверке.</li></ul><h2 id="cons">Ограничения </h2><ul><li>🧱 <strong>Бренды — это compile-time метки</strong>: во время выполнения они не существуют.</li><li>💡 <strong>Можно случайно обойти типизацию</strong>, используя <code>as</code> без валидации — будьте осторожны.</li><li>🧠 <strong>Требуется внимание разработчиков</strong>: особенно в больших командах важно соблюдать договорённости при создании брендированных типов.</li></ul><h2 id="use-cases">Применение на практике </h2><h3>1. Разделение ID-шников</h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'UserId'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PostId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'PostId'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getComments</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">postId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PostId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">authorId</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> UserId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">getComments</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(user.id, post.id) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// ❌ TypeScript выдаст ошибку</span></span></code></pre><h3>2. Валидация пользовательского ввода</h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> EmailAddress</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"EmailAddress"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> validateEmail</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">input</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> EmailAddress</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // Не валидируйте так email! Этот код написан для примера.</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">input.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"@"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="color:#D73A49;--shiki-dark:#F97583">throw</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Invalid email"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> input </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> EmailAddress</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3>3. Работа с числовыми значениями, например, возрастом</h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Age</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Brand</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Age"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createAge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">age</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Age</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (age </span><span style="color:#D73A49;--shiki-dark:#F97583">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#D73A49;--shiki-dark:#F97583"> ||</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> age </span><span style="color:#D73A49;--shiki-dark:#F97583">></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 125</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    throw</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Возраст вне допустимого диапазона"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> age </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Age</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getBirthYear</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">age</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Age</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">getFullYear</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() </span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> age;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> myAge</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createAge</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">30</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> birthYear</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getBirthYear</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(myAge);</span></span></code></pre><h2>Заключение</h2><p><strong>Branded Types</strong> — это мощный приём TypeScript, позволяющий повысить читаемость, надёжность и безопасность кода. Несмотря на то, что они не влияют на выполнение, их сила в compile-time проверках, помогающих предотвратить трудноуловимые ошибки.</p><p>Используйте <strong>брендированные типы</strong> там, где одинаковые по структуре значения имеют разное семантическое значение. Это особенно важно в больших приложениях, API-интеграциях и при валидации пользовательского ввода.</p><p>На этом всё. Спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/typescript-branded-types/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем Not Found (404) страницу в Next.js приложение]]></title>
            <link>https://baranov.guru/posts/nextjs-not-found/</link>
            <guid>https://baranov.guru/posts/nextjs-not-found</guid>
            <pubDate>Wed, 26 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению Not Found (404) страницы в Next.js приложение.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2025 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Важным элементом <strong>SEO-оптимизации</strong> является правильная работа с ошибками. В этой статье вы узнаете, что такое <strong>ошибка 404</strong>, и как правильно работать с ней в приложении на <strong>Next.js</strong>.</p><h2 id="what">Что такое Not Found или 404 страница? </h2><p>Ошибка <strong>404</strong>, также известная как «<strong>Not Found</strong>» или «<strong>Не найдено</strong>», является стандартным <strong>HTTP</strong>-статусным кодом, который указывает на отсутствие запрашиваемого ресурса на сервере.</p><h2 id="file">Файл not-found.js | not-found.ts </h2><p>В <strong>Next.js</strong>, начиная с <strong>13</strong> версии, для работы с отображением <strong>404</strong> ошибки используется файл <strong>not-found.js</strong>.</p><p>Вот так может выглядеть код файла:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-javascript"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Link </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/link"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> NotFound</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Не найдено&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Запрашиваемый ресурс не доступен&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Вернуться на главную&#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Для отображения <strong>404</strong> ошибки необходимо поместить данный файл в корень директории <strong>app/</strong>,</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>app/not-found.tsx</span></span></code></pre><p>страница будет отображаться для <strong>всех</strong> ненайденных <strong>URL</strong> приложения.</p><p>По умолчанию, <strong>NotFound</strong> - серверный компонент, и если необходимо получить какие-то данные перед отрисовкой, то необходимо сделать его асинхронным (<em>async</em>).</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Link </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'next/link'</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { headers } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'next/headers'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> NotFound</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> authorization</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> headers</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'authorization'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> res</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> fetch</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'...'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    headers: { authorization },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  })</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> user</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> res.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">json</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">     &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Ничего не найдено, {user.name}.&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Но ты можешь попробовать снова, вдруг это просто ошибка?&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Если же необходимо получить какие-то данные уже на клиенте, то можно вынести часть содержимого страницы в <strong>клиентский компонент</strong>, и запросить данные из него.</p><p>Подробную информацию можно найти в <a href="https://nextjs.org/docs/app/api-reference/file-conventions/not-found">официальной документации по странице</a>.</p><h2 id="function">Функция notFound() </h2><p>Так же отрисовку файла not-found.js можно вызвать принудительно с помощью функции <strong>notFound()</strong>.</p><p>Примерно вот так:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { notFound } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/navigation"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">params</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Params</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(params.slug);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> notFound</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Эта функция позволяет кинуть ошибку <strong>NEXT_NOT_FOUND</strong>, а затем вызвать рендер страницы <strong>not-found.js</strong> в пределах сегмента пути и отдать <strong>404 код ошибки HTTP</strong>.</p><p>В дополнение к этому, функция вставит на страницу <strong>мета-тег robots</strong>, при её использовании в серверном компоненте.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">meta</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"robots"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> content</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"noindex"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre><p>Функцию <strong>notFound()</strong> можно использовать в серверных компонентах (<strong>Server Components</strong>), обработчиках пути (<strong>Route Handlers</strong>) и серверных действиях (<strong>Server Actions</strong>).</p><p>Подробную информацию можно найти в <a href="https://nextjs.org/docs/app/api-reference/functions/not-found">официальной документации по функции</a>.</p><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-not-found/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем sitemap.xml в Next.js приложение]]></title>
            <link>https://baranov.guru/posts/nextjs-sitemap/</link>
            <guid>https://baranov.guru/posts/nextjs-sitemap</guid>
            <pubDate>Fri, 14 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению sitemap.xml в Next.js приложение.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2025 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Важным элементом <strong>SEO-оптимизации</strong> является наличие файла <strong>sitemap.xml</strong>. В этой статье вы узнаете, что такое <strong>sitemap.xml</strong> и как добавить его в свое приложение на <strong>Next.js</strong>.</p><h2 id="what">Что такое sitemap.xml? </h2><p><strong>Sitemap.xml</strong> - это файл формата <a href="https://www.sitemaps.org/protocol.html">Sitemaps XML format</a>, который сообщает поисковым системам (например <strong>Google</strong> или <strong>Yandex</strong>), какие страницы есть на вашем сайте и как часто они обновляются.</p><p>Этот файл должен находиться в корневой папке вашего сайта, и обычно доступен по адресу:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>домен.зона/sitemap.xml</span></span></code></pre><p>Например, <strong>sitemap.xml</strong> этого блога лежит по адресу <a href="https://baranov.guru/sitemap.xml">https://baranov.guru/sitemap.xml</a> и выглядит следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>&#x3C;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>  ...</span></span>
<span class="line"><span>&#x3C;/urlset></span></span></code></pre><p>Более подробную информацию о том как как поисковые системы обрабатывают файлы <strong>sitemap.xml</strong> можно найти в <a href="https://yandex.ru/support/webmaster/controlling-robot/sitemap.html">справке от Яндекса</a> или <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview">документации Google</a>.</p><h2 id="static">Первый способ: Добавляем статический файл </h2><p>Первый способ - очень простой. <strong>Next.js</strong> из коробки умеет отдавать статические файлы, например изображения, добавленные в папку <strong>public</strong> в корне проекта.</p><p>Этот же подход можно использовать для добавления файла <strong>sitemap.xml</strong>. Всё что необходимо сделать - добавить файл в папку <strong>public</strong>.</p><p>Подробнее про работу со статикой и папку <strong>public</strong> можно почитать в <a href="https://nextjs.org/docs/app/building-your-application/optimizing/static-assets">официальной документации Next.js</a>.</p><p>Как видите, способ действительно очень простой, но подойдёт он вам только в случае, если у вас небольшой сайт, с редко меняющимся содержимым. В противном случае лучше прибегнуть к <a href="#dynamic">динамической генерации sitemap.xml</a>.</p><h3 id="alternate">Альтернативный способ добавления статического файла </h3><p>Начиная с <a href="https://github.com/vercel/next.js/releases/tag/v13.3.0">версии 13.3</a> <strong>Next.js</strong> умеет корректно обрабатывать статический файл <strong>sitemap.xml</strong> лежащий в корне папки <strong>app</strong>.</p><p>Подробнее в <a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap">документации</a>.</p><h2 id="dynamic">Второй способ: Динамическая генерация sitemap.xml </h2><p>Для того чтобы динамически создавать файл <strong>sitemap.xml</strong> необходимо в папку <strong>app</strong> добавить файл <strong>robots.js</strong> или <strong>robots.ts</strong>, который будет возвращать объект типа <a href="#type"><strong>MetadataRoute.Sitemap</strong></a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { URL_BASE } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/lib/constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/about/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Данный код сгенерирует файл со следующим содержимым:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>&#x3C;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/about/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>    &#x3C;/url></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/posts/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>&#x3C;/urlset></span></span></code></pre><h3 id="image">Генерация Image Sitemaps </h3><p>Так же, начиная с <strong>15</strong> версии в <strong>Next.js</strong> есть встроенная возможность генерировать <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps"><strong>Image Sitemaps</strong></a>. Для этого надо всего лишь добавить свойство <strong>images</strong> (см. <a href="#type">спецификацию</a>) к странице.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { URL_BASE } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/lib/constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      images: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/assets/authors/avatar.webp"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>В результате получим следующий файл:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>&#x3C;urlset</span></span>
<span class="line"><span>  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"</span></span>
<span class="line"><span>  xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"</span></span>
<span class="line"><span>></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;image:image></span></span>
<span class="line"><span>      &#x3C;image:loc>https://baranov.guru/assets/authors/avatar.webp&#x3C;/image:loc></span></span>
<span class="line"><span>    &#x3C;/image:image></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>&#x3C;/urlset></span></span></code></pre><h3 id="video">Генерация Video Sitemaps </h3><p>Так же, начиная с <strong>15</strong> версии в <strong>Next.js</strong> есть встроенная возможность генерировать <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps"><strong>Video Sitemaps</strong></a>. Для этого надо всего лишь добавить свойство <strong>videos</strong> (см. <a href="#type">спецификацию</a>) к странице.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { URL_BASE } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/lib/constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/yandex-gpt-intro/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      videos: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          title: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Название видео"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          thumbnail_loc:</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            "https://baranov.guru/assets/posts/yandex-gpt-intro/cover.webp"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          description: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Это описание видео"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      ],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>В результате получим следующий файл:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>&#x3C;urlset</span></span>
<span class="line"><span>  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"</span></span>
<span class="line"><span>  xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"</span></span>
<span class="line"><span>></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/posts/yandex-gpt-intro/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;video:video></span></span>
<span class="line"><span>      &#x3C;video:title>Название видео&#x3C;/video:title></span></span>
<span class="line"><span>      &#x3C;video:thumbnail_loc>https://baranov.guru/assets/posts/yandex-gpt-intro/cover.webp&#x3C;/video:thumbnail_loc></span></span>
<span class="line"><span>      &#x3C;video:description>Это описание видео&#x3C;/video:description></span></span>
<span class="line"><span>    &#x3C;/video:video></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>&#x3C;/urlset></span></span></code></pre><h3 id="locale">Генерация локализованных Sitemaps </h3><p>В <strong>Next.js</strong> есть встроенная возможность добавлять ссылки на версии страниц на другом языке при генерации <strong>sitemap.xml</strong>. Для этого надо всего лишь добавить свойство <strong>alternates</strong> (см. <a href="#type">спецификацию</a>) к странице.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { URL_BASE } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/lib/constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      changeFrequency: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"daily"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      alternates: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        languages: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          en: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/en/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          fr: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/fr/`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>В результате получим следующий файл:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>&#x3C;urlset</span></span>
<span class="line"><span>  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"</span></span>
<span class="line"><span>  xmlns:xhtml="http://www.w3.org/1999/xhtml"</span></span>
<span class="line"><span>></span></span>
<span class="line"><span>  &#x3C;url></span></span>
<span class="line"><span>    &#x3C;loc>https://baranov.guru/&#x3C;/loc></span></span>
<span class="line"><span>    &#x3C;xhtml:link</span></span>
<span class="line"><span>      rel="alternate"</span></span>
<span class="line"><span>      hreflang="en"</span></span>
<span class="line"><span>      href="https://baranov.guru/en/"/></span></span>
<span class="line"><span>    &#x3C;xhtml:link</span></span>
<span class="line"><span>      rel="alternate"</span></span>
<span class="line"><span>      hreflang="fr"</span></span>
<span class="line"><span>      href="https://baranov.guru/fr/"/></span></span>
<span class="line"><span>    &#x3C;lastmod>2025-03-13T12:07:00.337Z&#x3C;/lastmod></span></span>
<span class="line"><span>    &#x3C;changefreq>daily&#x3C;/changefreq></span></span>
<span class="line"><span>    &#x3C;priority>0.7&#x3C;/priority></span></span>
<span class="line"><span>  &#x3C;/url></span></span>
<span class="line"><span>&#x3C;/urlset></span></span></code></pre><h3 id="type">Спецификация MetadataRoute.Sitemap </h3><p>Вот полная спецификация MetadataRoute.Sitemap, взятая из <a href="https://github.com/vercel/next.js">официального репозитория на Github</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Sitemap</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  lastModified</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  changeFrequency</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "always"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "hourly"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "daily"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "weekly"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "monthly"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "yearly"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "never"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  priority</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  alternates</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        languages</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Languages</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  images</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] </span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  videos</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Videos</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] </span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}>;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Videos</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  title</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  thumbnail_loc</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  description</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  content_loc</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  player_loc</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  duration</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  expiration_date</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  rating</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  view_count</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  publication_date</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  family_friendly</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "yes"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "no"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  restriction</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Restriction</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  platform</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Restriction</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  requires_subscription</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "yes"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "no"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  uploader</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        info</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        content</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  live</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "yes"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "no"</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  tag</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> undefined</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h2 id="packages">Альтернативные решения </h2><p>Так же есть несколько альтернативных решений для генерации <strong>sitemap.xml</strong> в виде npm-пакетов. Например пакет <a href="https://www.npmjs.com/package/next-sitemap">next-sitemap</a>.</p><h2 id="validation">Бонус: Валидация sitemap.xml </h2><p>Для того чтобы удостовериться что содержимое <strong>sitemap.xml</strong> валидно и будет работать как и задумано, можно воспользоваться несколькими <strong>бесплатными</strong> инструментами:</p><div><p><img src="https://baranov.guru/assets/tools/sitemap-validator/cover.webp" alt="Валидатор Sitemap.xml"></p><p><a href="https://baranov.guru/tools/sitemap-validator/">Валидатор Sitemap.xml</a> — Проверка структуры XML sitemap: urlset/sitemapindex, loc/lastmod/changefreq/priority, типовые ошибки и замечания — онлайн.</p></div><ul><li><a href="https://search.google.com/search-console/sitemaps">Валидатор от Google</a>;</li><li><a href="https://webmaster.yandex.ru/tools/sitemap/">Валидатор от Yandex</a>;</li></ul><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-sitemap/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем robots.txt в Next.js приложение]]></title>
            <link>https://baranov.guru/posts/nextjs-robots/</link>
            <guid>https://baranov.guru/posts/nextjs-robots</guid>
            <pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению robots.txt в Next.js приложение.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2025 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Важным элементом <strong>SEO-оптимизации</strong> является наличие файла <strong>robots.txt</strong>. В этой статье вы узнаете, что такое <strong>robots.txt</strong> и как добавить его в свое приложение на <strong>Next.js</strong>.</p><h2 id="what">Что такое robots.txt? </h2><p><strong>Robots.txt</strong> - это текстовый файл, основанный на спецификации <a href="https://www.rfc-editor.org/rfc/rfc9309.html">Robots Exclusion Protocol</a>, который сообщает поисковым роботам (например <strong>Googlebot</strong> или <strong>Yandex</strong>), какие страницы они могут сканировать, а какие нет.</p><p>А так же указывает на файл <strong>sitemap.xml</strong>. О том что это такое и как добавить файл <strong>sitemap.xml</strong> читайте в статье «<a href="https://baranov.guru/posts/nextjs-sitemap/">Добавляем sitemap.xml в Next.js приложение</a>».</p><p><strong>Robots.txt</strong> должен находиться в корневой папке вашего сайта, и обычно доступен по адресу:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>домен.зона/robots.txt</span></span></code></pre><p>Например, у этого блога он лежит по адресу <a href="https://baranov.guru/robots.txt">https://baranov.guru/robots.txt</a> и выглядит следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span># *</span></span>
<span class="line"><span>User-agent: *</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Host</span></span>
<span class="line"><span>Host: https://baranov.guru/</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Sitemap</span></span>
<span class="line"><span>Sitemap: https://baranov.guru/sitemap.xml</span></span></code></pre><p>Более подробную информацию о том как как поисковые системы обрабатывают файлы <strong>robots.txt</strong> можно найти в <a href="https://yandex.ru/support/webmaster/controlling-robot/robots-txt.html">справке от Яндекса</a> или <a href="https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt">документации Google</a>.</p><h2 id="static">Первый способ: Добавляем статический файл </h2><p>Первый способ - очень простой. <strong>Next.js</strong> из коробки умеет отдавать статические файлы, например изображения, добавленные в папку <strong>public</strong> в корне проекта.</p><p>Этот же подход можно использовать для добавления файла <strong>robots.txt</strong>. Всё что необходимо сделать - добавить файл в папку <strong>public</strong>.</p><p>Подробнее про работу со статикой и папку <strong>public</strong> можно почитать в <a href="https://nextjs.org/docs/app/building-your-application/optimizing/static-assets">официальной документации Next.js</a>.</p><h3 id="alternate">Альтернативный способ добавления статического файла </h3><p>Начиная с <a href="https://github.com/vercel/next.js/releases/tag/v13.3.0">версии 13.3</a> <strong>Next.js</strong> умеет корректно обрабатывать статический файл <strong>robots.txt</strong> лежащий в корне папки <strong>app</strong>.</p><p>Подробнее в <a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots#static-robotstxt">документации</a>.</p><h2 id="dynamic">Второй способ: Динамическая генерация robots.txt </h2><p>Для того чтобы динамически создавать файл <strong>robots.txt</strong> необходимо в папку <strong>app</strong> добавить файл <strong>robots.js</strong> или <strong>robots.ts</strong>, который будет возвращать объект типа <a href="#type"><strong>MetadataRoute.Robots</strong></a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> robots</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Robots</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    rules: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      userAgent: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"*"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      allow: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      disallow: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/private/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    sitemap: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/sitemap.xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    host: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Данный код сгенерирует файл со следующим содержимым:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>User-Agent: *</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span>Disallow: /private/</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Host: https://baranov.guru/</span></span>
<span class="line"><span>Sitemap: https://baranov.guru/sitemap.xml</span></span></code></pre><h3 id="custom">Кастомизация настроек в зависимости от user-agent </h3><p>Так же есть возможность добавлять правила для различных <strong>user-agent</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> robots</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Robots</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    rules: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        userAgent: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Googlebot"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        allow: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        disallow: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/private/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        userAgent: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Yandex"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Bingbot"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        disallow: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    sitemap: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/sitemap.xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>На выходе получим следующий файл:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>User-Agent: Googlebot</span></span>
<span class="line"><span>Allow: /</span></span>
<span class="line"><span>Disallow: /private/</span></span>
<span class="line"><span></span></span>
<span class="line"><span>User-Agent: Yandex</span></span>
<span class="line"><span>User-Agent: Bingbot</span></span>
<span class="line"><span>Disallow: /</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Sitemap: https://baranov.guru/sitemap.xml</span></span></code></pre><h3 id="type">Спецификация MetadataRoute.Robots </h3><p>Вот полная спецификация MetadataRoute.Robots, взятая из <a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots#robots-object">официальной документации</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Robots</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  rules</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        userAgent</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        allow</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        disallow</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        crawlDelay</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    |</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Array</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        userAgent</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        allow</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        disallow</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">        crawlDelay</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }>;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  sitemap</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  host</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h3 id="dynamic-cons">Минусы динамической генерации и директива Clean-Param </h3><p>К сожалению, динамическая генерация <strong>не поддерживает добавление кастомных директив</strong> в правила. Самым ярким примером кастомной директивы можно считать <a href="https://yandex.ru/support/webmaster/robot-workings/clean-param.html">директиву Clean-Param</a>, поддерживаемую <strong>Яндексом</strong>.</p><p>Для того чтобы её добавить придётся воспользовать либо <a href="#static">первым способом (статический файл)</a> либо каким-нибудь сторонним решением, например <strong>npm-пакетом</strong> <a href="https://www.npmjs.com/package/next-sitemap">next-sitemap</a>.</p><h2 id="validation">Бонус: Валидация robots.txt </h2><p>Для того чтобы удостовериться что содержимое <strong>robots.txt</strong> валидно и будет работать как и задумано, можно воспользоваться несколькими <strong>бесплатными</strong> инструментами:</p><div><p><img src="https://baranov.guru/assets/tools/robots-txt-validator/cover.webp" alt="Валидатор Robots.txt"></p><p><a href="https://baranov.guru/tools/robots-txt-validator/">Валидатор Robots.txt</a> — Проверка синтаксиса и структуры robots.txt: User-agent, Allow, Disallow, Sitemap, директивы для поисковых систем — онлайн.</p></div><ul><li><a href="https://www.google.com/webmasters/tools/robots-testing-tool">Валидатор от Google</a>;</li><li><a href="https://webmaster.yandex.ru/tools/robotstxt/">Валидатор от Yandex</a>;</li></ul><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-robots/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем кнопку для сбора пожертвований (донатов) к блогу]]></title>
            <link>https://baranov.guru/posts/blog-donation-button/</link>
            <guid>https://baranov.guru/posts/blog-donation-button</guid>
            <pubDate>Wed, 03 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению кнопки для сбора пожертвований (донатов) к блогу.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Сегодня расскажем как добавить <strong>кнопку для сбора пожертвований (донатов)</strong> к блогу или сайту.</p><h2 id="why">Зачем блогеру нужна кнопка "Поддержать" </h2><p>Ведение блога — это не только творчество, но и регулярные усилия: подбор тем, написание, редактирование, поддержка сайта. Монетизация через рекламу не всегда подходит, а донаты позволяют читателю напрямую выразить благодарность. Даже небольшие суммы помогают автору продолжать развивать проект.</p><aside class="post-disclaimer" role="note"><p>ℹ️ По данным Patreon, регулярная поддержка позволяет авторам увеличивать доход в среднем на 25–40 % ежемесячно.</p></aside><h2 id="service">Выбор сервиса для сбора пожертвований </h2><p>Существует множество <strong>сервисов</strong>, которые позволяют собрать <strong>пожертвования (донаты)</strong> с пользователей:</p><p>Быстрый поиск в интернете выдал следующие результаты:</p><ul><li><strong>BuyMeACoffee</strong> - на первый взгляд - то что нужно, но находится за рубежом и не обслуживает клиентов из РФ;</li><li><strong>Ko-fi</strong> - аналогично <strong>BuyMeACoffee</strong>, но ещё говорят и кидает на деньги если ты из РФ;</li><li><a href="https://coindrop.to/">Coindrop</a> - интересный <strong>open-source</strong> сервис, но как мне показалось больше подходит для <strong>крипты</strong>;</li><li><a href="https://yookassa.ru/">ЮKassa</a> - всё хорошо, я им даже пользовался в <a href="https://baranov.guru/posts/unasprazdnik-fail/">"У нас праздник"</a>, но не работает с физ. лицами;</li><li><a href="https://yoomoney.ru/">ЮMoney</a> - та же <strong>ЮKassa</strong>, но вид сбоку;</li><li><a href="https://cloudtips.ru/">CloudTips</a> - сервис для чаевых и донатов от <strong>Т-банка</strong>, предназначен именно для физиков и безвозмездных пожертвований.</li></ul><table><thead><tr><th>Сервис</th><th>Комиссия</th><th>Платежные методы</th><th>Особенности</th></tr></thead><tbody><tr><td>BuyMeACoffee</td><td>~5%</td><td>Карты, Apple Pay, PayPal</td><td>Простая настройка, англ. интерфейс</td></tr><tr><td>Ko-fi</td><td>0% (в free), 5% (Gold)</td><td>PayPal</td><td>Простой виджет, поддержка целей</td></tr><tr><td>CloudTips</td><td>5%</td><td>Карты РФ</td><td><strong>Подходит для российских блогеров</strong></td></tr><tr><td>Donorbox</td><td>0–1.5% + Stripe/PayPal</td><td>Stripe, PayPal</td><td>Много кастомизации, подписки</td></tr><tr><td>ЮMoney/ЮKassa</td><td>3–6%</td><td>Карты, СБП</td><td><strong>Легальная интеграция в РФ</strong></td></tr></tbody></table><aside class="post-disclaimer" role="note"><p>⚠️ Перед выбором уточните юридические нюансы и условия вывода средств.</p></aside><p>В итоге я решил создать себе <a href="https://pay.cloudtips.ru/p/7d0e9b9f">страницу на CloudTips</a>.</p><h2 id="button">Как выглядит "правильная" кнопка доната (требования к кнопке) </h2><h3>Советы по оформлению:</h3><ul><li>Цвет — <strong>контрастный</strong>, выделяющийся на фоне сайта</li><li>Надпись — короткая и мотивирующая: <code>Поддержать</code>, <code>Купить кофе</code>, <code>Помочь блогу</code></li><li>Размер — не слишком маленький, кнопка должна быть заметной</li><li>Положение — в шапке, футере и внутри постов</li></ul><h3>Пример кнопки:</h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://www.buymeacoffee.com/yourname"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  target</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"_blank"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  rel</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"noopener noreferrer"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  class</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"donate-button"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ☕ Поддержать</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre><p>Стилизуем через CSS:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-css"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">.donate-button</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  background-color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">#ffdd00</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">#000</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  padding</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.6</span><span style="color:#D73A49;--shiki-dark:#F97583">em</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#D73A49;--shiki-dark:#F97583">em</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  font-weight</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">bold</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  border-radius</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">8</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  text-decoration</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">none</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  box-shadow</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 2</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 4</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  transition</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: transform </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.2</span><span style="color:#D73A49;--shiki-dark:#F97583">s</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">.donate-button:hover</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">scale</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1.05</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="where">Где размещать кнопку </h2><ul><li><strong>В шапке сайта</strong> — рядом с навигацией;</li><li><strong>В конце каждого поста</strong> — после текста;</li><li><strong>В сайдбаре</strong> (если есть);</li><li><strong>В футере</strong> — для постоянного доступа;</li><li><strong>Плавающая кнопка</strong> (например, снизу справа);</li></ul><h2 id="tricks">Продвинутые приёмы </h2><ul><li><strong>QR-код для доната</strong> — удобно если вас читают с десктопа, а платить будут с мобильного;</li><li><strong>Inline‑форма</strong> — некоторые сервисы позволяют вставлять форму прямо в пост;</li><li><strong>Цели/прогресс</strong> — показывайте цель сбора и текущий прогресс (мотивация!);</li></ul><h2 id="social">Интеграция в соцсетях </h2><ul><li><strong>Telegram</strong>: закрепите ссылку в описании канала</li><li><strong>YouTube</strong>: добавьте кнопку в описание профиля</li></ul><h2 id="faq">Частые вопросы </h2><p><strong>Нужно ли платить налоги?</strong></p><p><strong>Всё сложно</strong>, но на практике часто <strong>проще заплатить налог</strong>, чем потом объяснять и доказывать банку и налоговой что ты не верблюд.</p><p><strong>Какой сервис выбрать?</strong></p><p>Для международной аудитории — <strong>BuyMeACoffee</strong> или <strong>Donorbox</strong>. Для РФ — <strong>CloudTips</strong> или <strong>ЮMoney</strong>.</p><p><strong>Люди не донатят, что делать?</strong></p><p>Добавьте <strong>личный призыв</strong>, расскажите, <strong>зачем</strong> поддержка важна, дайте <strong>стимул</strong>: «донаты помогут выпустить книгу».</p><p>Кнопка доната — это не про навязчивость, а про возможность поддержать полезный контент. Сделайте её <strong>видимой, дружелюбной и понятной</strong> — и она начнёт работать.</p><h2 id="react">P.S. Моя реализации кнопки для донатов на react </h2><h3>Компонент <strong>DonateButton</strong></h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> classNames </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "classnames"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { FaHeart } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-icons/fa"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> styles </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./DonateButton.module.css"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> DONATION_LINK</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://pay.cloudtips.ru/p/7d0e9b9f"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> DonateButton</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">DONATION_LINK</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      rel</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"noreferrer"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      target</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"_blank"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#6F42C1;--shiki-dark:#B392F0">classNames</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "bg-red-600 rounded-md p-2 text-white flex flex-row gap-2 justify-center items-center text-xl"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        styles.wrap</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      )}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    ></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">FaHeart</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> size</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">20</span><span style="color:#24292E;--shiki-dark:#E1E4E8">} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{styles.heart} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>Отправить донат&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> DonateButton;</span></span></code></pre><h3>Стили для компонента</h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-css"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">.wrap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  animation</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: glow </span><span style="color:#005CC5;--shiki-dark:#79B8FF">800</span><span style="color:#D73A49;--shiki-dark:#F97583">ms</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ease-out</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> infinite</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> alternate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  border-color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">rgb</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">153</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">51</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">51</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  box-shadow</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    inset</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">rgb</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">238</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">238</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  outline</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">none</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">@keyframes</span><span style="color:#E36209;--shiki-dark:#FFAB70"> glow</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  0%</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    border-color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">rgb</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">153</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">51</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">51</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    box-shadow</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">      0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">      inset</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  100%</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    border-color</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">rgb</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">102</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">102</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    box-shadow</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">      0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 20</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.6</span><span style="color:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">      inset</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 10</span><span style="color:#D73A49;--shiki-dark:#F97583">px</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rgba</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">255</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.4</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">.heart</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  animation</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: heartbeat </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.9</span><span style="color:#D73A49;--shiki-dark:#F97583">s</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> linear</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> infinite</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">@keyframes</span><span style="color:#E36209;--shiki-dark:#FFAB70"> heartbeat</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  0%</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">scale</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1.05</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  50%</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">scale</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  100%</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    transform</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">scale</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.9</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/blog-donation-button/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем поддержку MDX в Next.js приложение]]></title>
            <link>https://baranov.guru/posts/nextjs-mdx/</link>
            <guid>https://baranov.guru/posts/nextjs-mdx</guid>
            <pubDate>Sun, 23 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению поддержки MDX в Next.js приложение.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Сегодня расскажем как можно добавить базовую поддержку <strong>MDX</strong> в <strong>Next.js</strong> приложение и использовать <strong>.mdx</strong> файлы в качестве страниц.</p><h2 id="what">Что такое MDX? </h2><p><strong>MDX</strong> - это синтаксис, который позволяет использовать <strong>JSX</strong>-синтаксис в <strong>markdown</strong> контенте, ну или наоборот. Подробнее можно почитать <a href="https://mdxjs.com/docs/what-is-mdx/">тут</a>.</p><p>Вот простой пример <strong>MDX</strong> синтаксиса:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-jsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ComplexComponent </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/app/_components/ComplexComponent"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ImageWrap </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/app/_components/ImageWrap"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8"># Тут заголовок</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">обычный текст</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">и React</span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#6F42C1;--shiki-dark:#B392F0">компоненты</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> например, для картинок</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">ImageWrap</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"путь к картинке"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}/></span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> или что</span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8">нибудь сложное</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">ComplexComponent</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre><h2>Описание процесса</h2><p>Итак, для того чтобы добавить поддержку <strong>MDX</strong> в <strong>Next.js</strong> приложение необходимо:</p><ul><li><a href="#packages">Установить необходимые пакеты</a>;</li><li><a href="#file">Создать файл mdx-components.tsx</a>;</li><li><a href="#config">Включить обработку .mdx файлов в next.config.js</a>;</li><li><a href="#extension">(Опционально) Настроить IDE</a>;</li></ul><h2 id="packages">Установка необходимых пакетов </h2><p>Нам понадобятся пакеты:</p><ul><li><a href="https://www.npmjs.com/package/@next/mdx">@next/mdx</a> - основной пакет, который дружит <strong>Next.js</strong> и <strong>MDX</strong>;</li><li><a href="https://www.npmjs.com/package/@mdx-js/loader">@mdx-js/loader</a> - <strong>webpack loader</strong> для <strong>MDX</strong> файлов;</li><li><a href="https://www.npmjs.com/package/@types/mdx">@types/mdx</a> - необходим для нормальной работы с <strong>typescript</strong>;</li></ul><p>Установим их следующими командами:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> -S</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @next/mdx</span></span></code></pre><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> -D</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @mdx-js/loader</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @types/mdx</span></span></code></pre><h2 id="file">Создание файла mdx-components.tsx </h2><p>Теперь необходимо создать файл <strong>mdx-components.tsx</strong> и положить его в <strong>src/</strong>.</p><p>Файл <strong>mdx-components.tsx</strong> необходим, для того чтобы подружить <strong>MDX</strong> и <strong>Next.js</strong> приложение использующее <strong>App Router</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MDXComponents } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "mdx/types"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useMDXComponents</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">components</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MDXComponents</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MDXComponents</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    ...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">components,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="config">Включение обработки .mdx файлов в next.config.js </h2><p>Осталось только подправить <strong>конфиг</strong> (файл <strong>next.config.js</strong>) нашего приложения.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> withMDX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"@next/mdx"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">/** </span><span style="color:#D73A49;--shiki-dark:#F97583">@type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> {import('next').NextConfig}</span><span style="color:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextConfig</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // Добавляем `mdx` в pageExtensions</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  pageExtensions: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"jsx"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"mdx"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"tsx"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> withMDX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(nextConfig);</span></span></code></pre><p>Теперь вместо <strong>page.tsx</strong> для страниц можно использовать <strong>page.mdx</strong>! 🎉</p><h2 id="extension">Расширение для VSCode </h2><p>Бонус для тех кто использует <strong>VSCode</strong>, есть неплохое расширение для поддержки MDX синтаксиса - <a href="https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx">MDX for Visual Studio Code</a>;</p><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-mdx/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Как посчитать время чтения текста (и добавить индикатор на сайт)]]></title>
            <link>https://baranov.guru/posts/blog-post-read-time/</link>
            <guid>https://baranov.guru/posts/blog-post-read-time</guid>
            <pubDate>Sat, 22 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению индикатора времени чтения текста на сайт.]]></description>
            <content:encoded><![CDATA[<p>Индикатор времени чтения — простая деталь, которая <strong>улучшает пользовательский опыт</strong>: читатель сразу понимает, сколько времени займёт статья.</p><p>В этой статье разберём:</p><ul><li>как считается время чтения;</li><li>какую скорость брать за основу;</li><li>как реализовать расчёт на JavaScript / TypeScript.</li></ul><h2 id="quick">Как быстро посчитать время чтения текста </h2><aside class="post-disclaimer" role="note"><p>Если не хочется считать вручную, проще проверить в готовом инструменте — например, в нашем <a href="https://baranov.guru/tools/text-calculator/">текстовом калькуляторе</a>.</p></aside><p>Базовая логика очень простая:</p><ul><li>считаем <strong>количество слов</strong> в тексте;</li><li>делим на <strong>среднюю скорость</strong> чтения.</li></ul><h3 id="formula">Формула расчёта </h3><p>Формула выглядит так:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>время чтения (мин) = количество слов / скорость чтения</span></span></code></pre><p>Чтобы избежать значения 0 минут, результат обычно округляют вверх.</p><h3 id="calculation-example">Пример расчёта </h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>700 слов / 140 слов в минуту ≈ 5 минут</span></span></code></pre><h2 id="speed">Какая скорость чтения считается нормальной </h2><p>Единого стандарта нет, но обычно используют такие значения:</p><ul><li><strong>100–120</strong> слов/мин — сложный или технический текст</li><li><strong>130–160</strong> слов/мин — обычные статьи</li><li><strong>180+</strong> слов/мин — лёгкое чтение</li></ul><p>Для блога с техническим контентом разумно брать около <strong>140</strong> слов в минуту — это даёт более реалистичную оценку.</p><h2 id="count-words">Подсчёт количества слов </h2><p>Начнём с функции подсчёта слов. Один из самых простых вариантов — использовать <strong>регулярное выражение</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> countWords</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">str</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> words</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> str.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">match</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">[ЁёА-я\w\d</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\’\'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">-]</span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gi</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> words </span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> words.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h3 id="cyrillic-words">Важный момент </h3><p>Обратите внимание на часть:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">ЁёА</span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#24292E;--shiki-dark:#E1E4E8">я</span></span></code></pre><p>Без неё кириллические слова не будут учитываться, и результат окажется некорректным (например, будет считаться только код или латиница).</p><h3 id="count-words-tests">Тесты для функции подсчёта слов </h3><p>После этого я написал несколько тестов для функции <strong>countWords</strong>, вот один из них для примера:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">it</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"should return correct value for cyrillic string"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // arrange</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> expectedResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> inputValue</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "тест"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // act</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> countWords</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(inputValue);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // assert</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  expect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(result).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toBe</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(expectedResult);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre><h2 id="read-time">Расчёт скорости чтения </h2><p>Теперь подсчитаем количество слов:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> AVERAGE_READ_SPEED</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 140</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">//Слов в минуту</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getReadTime</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">content</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#6F42C1;--shiki-dark:#B392F0">countWords</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(content) </span><span style="color:#D73A49;--shiki-dark:#F97583">/</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> AVERAGE_READ_SPEED</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h3 id="ceil">Не забудьте про округление </h3><p>Обратите внимание на вызов:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre><p>Для того чтобы не получать <strong>0 минут</strong>, решили <strong>округлять результат вверх</strong> до ближайшего целого числа.</p><h3 id="read-time-tests">Тесты для функции подсчёта времени чтения </h3><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">it</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"should return 0 for an empty string"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // arrange</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> expectedResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> inputValue</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // act</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getReadTime</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(inputValue);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // assert</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  expect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(result).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toBe</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(expectedResult);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">it</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"should return 1 for not empty string"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // arrange</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> expectedResult</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> inputValue</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "тест"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // act</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getReadTime</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(inputValue);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  // assert</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  expect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(result).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toBe</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(expectedResult);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre><h2 id="disclaimer">Ограничения подхода </h2><p>Важно понимать, что это приближённая оценка:</p><ul><li>не учитывается сложность текста;</li><li>не учитываются изображения, код и форматирование;</li><li>скорость чтения у разных людей отличается.</li></ul><p>Тем не менее, для большинства блогов такой расчёт даёт достаточно точный результат.</p><h2>Где это может пригодиться</h2><p>Индикатор времени чтения полезен не только разработчикам, но и:</p><ul><li>авторам блогов;</li><li>копирайтерам;</li><li>SEO-специалистам;</li><li>редакторам;</li><li>студентам;</li></ul><p>Это простой способ сделать контент более предсказуемым для читателя.</p><h2 id="text-calculator">Как проверить результат </h2><p>Если вы хотите быстро проверить любой текст без кода, можно воспользоваться <a href="https://baranov.guru/tools/text-calculator/">текстовым калькулятором</a>: вставить текст и сразу получить количество слов и примерное время чтения.</p><div><p><img src="https://baranov.guru/assets/tools/text-calculator/cover.webp" alt="Текстовый калькулятор"></p><p><a href="https://baranov.guru/tools/text-calculator/">Текстовый калькулятор</a> — Проверьте ваш текст за минуту: найдёт длину текста, количество слов, абзацев и примерное время чтения.</p></div><p>На этом всё! <strong>Спасибо</strong> за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/blog-post-read-time/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем подсветку кода (синтаксиса) в статический блог на Next.js]]></title>
            <link>https://baranov.guru/posts/nextjs-prismjs/</link>
            <guid>https://baranov.guru/posts/nextjs-prismjs</guid>
            <pubDate>Sat, 15 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению подсветки кода (синтаксиса) в статическом блоге на Next.js.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Продолжаем работать над улучшением <a href="https://baranov.guru">блога</a>. В этой статье расскажем как добавляли <strong>подсветку кода (синтаксиса)</strong> в постах.</p><p>Есть множество способов сделать это, мы же решили пойти самым простым путём и остановились на использовании пакета <a href="https://www.npmjs.com/package/prismjs">Prism</a>.</p><p>Процесс можно разделить на несколько этапов:</p><ul><li><a href="#install">Установка необходимых пакетов</a></li><li><a href="#generate">Статическая генерация подсвеченного кода</a></li><li><a href="#languages">Добавление подсветки необходимых языков</a></li><li><a href="#plugin">Работа с плагинами Prism</a></li><li><a href="#theme">Подключение темы Prism</a></li></ul><h2 id="install">Установка необходимых пакетов </h2><p>Сначала установим необходимые пакеты:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> prismjs</span></span></code></pre><p>Так как мы статически генерируем <strong>html</strong> блога из <strong>.md</strong> файлов, то нам нужен способ запускать <strong>Prism</strong> во время сборки приложения. В этом нам поможет пакет <a href="https://www.npmjs.com/package/rehype-prism">rehype-prism</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> rehype-prism</span></span></code></pre><h2 id="generate">Статическая генерация подсвеченного кода </h2><p>В подробностях тут описывать особенно нечего, просто импортируем необходимые пакеты и включаем их в процесс генерации <strong>html</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> rehypePrism </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "rehype-prism"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> rehypeStringify </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "rehype-stringify"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> remarkCustomHeaderId </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "remark-custom-header-id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> remarkParse </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "remark-parse"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> remarkRehype </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "remark-rehype"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { unified } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "unified"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> markdownToHtml</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">markdown</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> unified</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkParse, { sanitize: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">false</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkCustomHeaderId)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkRehype)</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // Внимание на следующую строку</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(rehypePrism)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(rehypeStringify)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">process</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(markdown);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> result).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="languages">Добавление подсветки необходимых языков </h2><p><strong>Prism</strong> поддерживает множество языков, но для того чтобы они заработали, необходимо подключить их.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/components/prism-bash"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/components/prism-json"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/components/prism-typescript"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><h2 id="plugin">Работа с плагинами Prism </h2><p>Так же <strong>Prism</strong> поддерживает множество плагинов. Мы выбрали <strong>"line-numbers"</strong>, <strong>"toolbar"</strong>, <strong>"copy-to-clipboard"</strong> плагины.</p><p>Для того чтобы их активировать необходимо:</p><ul><li><a href="#import-plugins">Подключить код и стили этих плагинов</a>;</li><li><a href="#use-plugins">Указать пакету <strong>rehype-prism</strong> что их необходимо использовать</a>.</li></ul><h3 id="import-plugins">Подключение кода и стилей этих плагинов </h3><p>Добавим импорт стилей для плагинов в компонент <strong>Post</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/plugins/line-numbers/prism-line-numbers.css"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/plugins/toolbar/prism-toolbar.css"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><h3 id="use-plugins">Использование плагинов </h3><p>Пакет <strong>rehype-prism</strong> поддерживает нужные нам плагины из коробки, необходимо только указать что их следует использовать.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> markdownToHtml</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">markdown</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> unified</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkParse, { sanitize: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">false</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> })</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkCustomHeaderId)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(remarkRehype)</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // Внимание на следующую строку</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(rehypePrism, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      plugins: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"line-numbers"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"toolbar"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"copy-to-clipboard"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">use</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(rehypeStringify)</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">process</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(markdown);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> result).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="theme">Подключение цветовой темы Prism </h2><p><strong>Prism</strong> поддерживает множество цветовых тем. Для того чтобы использовать выбранную тему, необходимо импортировать <strong>.css</strong> файл нужной темы.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "prismjs/themes/prism.css"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><p>На этом всё! 🎉</p><p>Результат можно посмотреть в <a href="https://baranov.guru/posts/">любом из постов блога</a>.</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-prismjs/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем пагинацию в статический блог на Next.js]]></title>
            <link>https://baranov.guru/posts/blog-pagination/</link>
            <guid>https://baranov.guru/posts/blog-pagination</guid>
            <pubDate>Mon, 03 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению пагинации постов в статическом блоге на Next.js.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Продолжаем работать над улучшением <a href="https://baranov.guru">блога</a> и одновременно разбираться в <strong>Next.js</strong>. В этой статье расскажем как добавить пагинацию к статическим страницам блога.</p><p>Процесс можно разделить на несколько этапов:</p><ul><li><a href="#divide">Разделение логики страницы на динамическую и статическую</a></li><li><a href="#links">Добавление навигационного компонента</a></li><li><a href="#seo">Работа над поисковой выдачей</a></li></ul><p>Результат можно посмотреть в самом низу <a href="https://baranov.guru/posts">страницы с постами</a>.</p><h2 id="divide">Разделение логики страницы на динамическую и статическую </h2><p>Нам не хотелось менять структуру страниц сайта, поэтому мы решили что страницу будем прокидывать через <strong>query string</strong>.</p><p>Статические страницы в <strong>Next.js</strong> имеют ряд ограничений при работе с <strong>query string</strong>. По сути, единственный доступный в <strong>моём случае</strong> способ получить доступ к параметрам из <strong>URL</strong> - воспользоваться хуком <a href="https://nextjs.org/docs/app/api-reference/functions/use-search-params">useSearchParams</a> из <strong>клиентского компонента</strong>.</p><p>Для этого необходимо <strong>вынести получение данных</strong> о постах в код <strong>статической</strong> страницы, и передать их в <strong>клиентский компонент</strong>, который сможет <strong>отфильтровать</strong> их нужным нам способом и отрисовать нужный нам список постов.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Suspense } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> PostsPage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPostsMeta</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> tags</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllTags</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      // ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Suspense</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> fallback</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">StaticPosts</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{posts} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">tags</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{tags} />}></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">DynamicPosts</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{posts} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">tags</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{tags} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Suspense</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      // ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Заметьте что компонент <strong>DynamicPosts</strong> обёрнут в <strong>Suspense</strong>. Фоллбек будет отрендерен в <strong>build-time</strong>. А компонент <strong>DynamicPosts</strong> будет отрендерен уже <strong>во время исполнения кода</strong> страницы на клиенте.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// ...</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> DynamicPage</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">posts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">tags</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> searchParams</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useSearchParams</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> page</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(searchParams.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"page"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)) </span><span style="color:#D73A49;--shiki-dark:#F97583">||</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> maxPage</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ceil</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(posts.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#D73A49;--shiki-dark:#F97583"> /</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> POSTS_PER_PAGE</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pageOffset</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (page </span><span style="color:#D73A49;--shiki-dark:#F97583">-</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> POSTS_PER_PAGE</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pagePosts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> posts.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">slice</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(pageOffset, pageOffset </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> POSTS_PER_PAGE</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">pagePosts.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> notFound</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Posts</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{pagePosts} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">title</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Посты"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">withFilter</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> tags</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{tags} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">PageSelector</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> page</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{page} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">maxPage</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{maxPage} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h2 id="links">Добавление навигационного компонента </h2><p>Для навигационного компонента, будем использовать компонент <strong>Link</strong> из встроенного пакета <strong>next/link</strong> и прокидывать в параметр <strong>href</strong> необходимую нам страницу.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`./?page=${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">page</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}>{page}&#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre><p>Сильно углубляться в подробности реализации не будем, скажем лишь что в результате получилось вот так:</p><p><img src="https://baranov.guru/assets/posts/blog-pagination/page-selector.webp" alt="Навигационный компонент" title="Навигационный компонент"></p><h2 id="seo">Работа над поисковой выдачей </h2><p>Для того чтобы не получить дублирующиеся страницы в поиске, и не усложнить себе жизнь долгим процессом удалением дублей, необходимо сделать следующее:</p><ul><li><a href="#canonical">Добавить ссылку на каноническую страницу</a>;</li><li><a href="#robots">Добавить директиву Clean-Param в robots.txt</a>;</li></ul><h3 id="canonical">Добавление ссылки на каноническую страницу </h3><p>Для того чтобы правильно пройти через процесс <a href="https://developers.google.com/search/docs/crawling-indexing/canonicalization?hl=ru">нормализации</a>, необходимо добавить <strong>ссылку на каноническую страницу</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">link</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> rel</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"canonical"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/posts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /></span></span></code></pre><p>В <strong>Next.js</strong> есть специальное <a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata">Metadata API</a>, которое можно использовать как раз для этих целей.</p><p>Воспользуемся им:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> metadata</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Metadata</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  title: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Посты | Алексей Баранов. Блог"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  description: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Страница со всеми опубликованными постами"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  alternates: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    canonical: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`/posts`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h3 id="robots">Добавление директивы Clean-Param в robots.txt </h3><p><strong>Яндекс</strong> поддерживает специальную <a href="https://yandex.ru/support/webmaster/robot-workings/clean-param.html">директиву Clean-param</a> в файлах <strong>robots.txt</strong>.</p><p>Давайте добавим её:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>User-agent: Yandex</span></span>
<span class="line"><span>Clean-param: page</span></span>
<span class="line"><span>Allow: /</span></span></code></pre><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/blog-pagination/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем поиск по тегам в статический блог на Next.js]]></title>
            <link>https://baranov.guru/posts/blog-tags/</link>
            <guid>https://baranov.guru/posts/blog-tags</guid>
            <pubDate>Sun, 02 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению простейшего поиска по тегам в статическом Next.js блоге.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Количество постов в <a href="https://baranov.guru">блоге</a> понемногу <strong>прибавляется</strong>, а находить их становится <strong>сложнее</strong>, поэтому мы решили добавить <strong>поиск постов по тегам</strong>.</p><p>Вот так он выглядит на момент написания этой статьи:</p><p><img src="https://baranov.guru/assets/posts/blog-tags/result.webp" alt="Результат" title="Результат"></p><p>Процесс можно разделить на несколько этапов:</p><ul><li><a href="#add-tags">Добавление тегов к статьям</a></li><li><a href="#page-generation">Генерация страниц для каждого тега</a></li><li><a href="#links">Добавление ссылок на страницы</a></li><li><a href="#seo">Работа над поисковой выдачей</a></li></ul><h2 id="add-tags">Добавление тегов к постам </h2><p>Тут всё просто, как и <a href="https://baranov.guru/posts/post-sharing-and-recommendations#recomendations">в случае с рекомендациями</a>, мы решили захардкодить в <strong>frontmatter</strong> каждой статьи массив с тегами.</p><p>Например, для <a href="https://baranov.guru/posts/blog-tags">этой статьи</a> массив выглядит следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">tags: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"улучшения блога"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"nextjs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre><p>Если вы пойдёте этим же путём, то <strong>обратите внимание</strong> на то, что в названии тегов лучше не использовать <strong>кириллицу и спец. символы</strong>. О том как всё же использовать в тегах кириллицу читайте <a href="#interesting">далее в статье</a>.</p><h2 id="page-generation">Генерация страниц для каждого тега </h2><p>Генерация страниц для тегов очень похожа на генерацию страниц постов.</p><p>Алгоритм очень простой и выглядит следующим образом:</p><ul><li><a href="#collect-tags">Проходим по всем постам и собираем уникальные теги</a></li><li><a href="#generate">Для каждого тега генерируем страницу с постами</a></li></ul><h3 id="collect-tags">Сбор уникальных тегов </h3><p>О том как получить все посты и их метаданные мы писали <a href="https://baranov.guru/posts/nextjs-rss#frontmatter">в статье о добавлении RSS фида</a>.</p><p>Там же можно найти и полную реализацию метода <strong>getAllPosts</strong>.</p><p>А нам же остаётся только добавить все теги в массив и удалить оттуда <strong>дубли</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllTags</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> notUniqueTags</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">flatMap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    p.tags </span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">p.tags] </span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Array.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">from</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Set</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(notUniqueTags));</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="generate">Генерация страниц </h3><p>Для того чтобы сгенерировать страницы, нам нужен не только список тегов, но и список постов по тегу:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPostsByTag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">tag</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">)</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> p.tags?.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">includes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(tag));</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Далее добавляем <strong>компонент для отображения</strong> страницы:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Params</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  params</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    tag</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Компонент для отображения страницы</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> TagPage</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">params</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Params</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> decodedTag</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getTagFromURI</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(params.tag);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPostsByTag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(decodedTag);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">posts.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> notFound</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      // Отображаем тут наши посты</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>И генерируем <strong>статические</strong> страницы:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// Генерируем статические страницы в build-time</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> generateStaticParams</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> tags</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllTags</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> tags.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">t</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    tag: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">getTagURI</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(t),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }));</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="interesting">Неочевидные моменты </h3><p>Вы наверняка заметили что мы используем функции <strong>getTagFromURI</strong> и <strong>getTagURI</strong> при работе с тегами.</p><p>Дело в том что на данный момент в <strong>Next.js</strong> <a href="https://github.com/vercel/next.js/issues/10084">существуют сложности с кириллическими путями</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getTagURI</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">tag</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="color:#D73A49;--shiki-dark:#F97583"> ===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "development"</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> encodeURIComponent</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(tag) </span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> tag;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getTagFromURI</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">uri</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> decodeURIComponent</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(uri);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h2 id="links">Добавление ссылок на страницы </h2><p>Тут сказать особо нечего, единственный момент, который следует учитывать - не забывайте оборачивать теги в <strong>encodeURIComponent</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> TagLinks</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> tags</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllTags</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      {tags.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">item</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">i</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">          href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`/tags/${</span><span style="color:#6F42C1;--shiki-dark:#B392F0">getTagURI</span><span style="color:#032F62;--shiki-dark:#9ECBFF">(</span><span style="color:#24292E;--shiki-dark:#E1E4E8">item</span><span style="color:#032F62;--shiki-dark:#9ECBFF">)</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">          key</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{i}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          {</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`#${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">item</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Link</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      ))}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h2 id="seo">Работа над поисковой выдачей </h2><aside class="post-disclaimer" role="note"><p>ℹ️ Подробнее про работу с <strong>sitemap</strong> можно прочитать в статье <a href="https://baranov.guru/posts/nextjs-sitemap/">Добавляем sitemap.xml в Next.js приложение</a>;</p></aside><p>Для улучшения индексирования сгенерированные страницы можно добавить в файл <strong>sitemap</strong> сайта.</p><p>Для этого, в файл <strong>sitemap.ts</strong>, лежащий в корне сайта добавляем следующий код:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { MetadataRoute } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { getAllTags, getTagURI } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/lib/api"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> MetadataRoute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Sitemap</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> tags</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllTags</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // тут остальные страницы сайта</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    ...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">tags.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">tag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://baranov.guru/tags/${</span><span style="color:#6F42C1;--shiki-dark:#B392F0">getTagURI</span><span style="color:#032F62;--shiki-dark:#9ECBFF">(</span><span style="color:#24292E;--shiki-dark:#E1E4E8">tag</span><span style="color:#032F62;--shiki-dark:#9ECBFF">)</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      lastModified: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      priority: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">0.7</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    })),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/blog-tags/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем рекомендации постов и блок "Поделиться в соц. сетях"]]></title>
            <link>https://baranov.guru/posts/post-sharing-and-recommendations/</link>
            <guid>https://baranov.guru/posts/post-sharing-and-recommendations</guid>
            <pubDate>Sun, 26 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению механизма рекомендаций постов и блока "Поделиться в соц. сетях".]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Изучали различные блоги, аналогичные <a href="https://baranov.guru">нашему</a> и решили добавить <a href="#social">блок "Поделиться в соц. сетях"</a>, а так же простейший <a href="#recomendations">механизм рекомендаций</a> статей. Вот что из этого вышло...</p><p><img src="https://baranov.guru/assets/posts/post-sharing-and-recommendations/result.webp" alt="Результат" title="Результат"></p><h2 id="recomendations">Добавляем рекомендации постов </h2><p>Тут всё просто, мы решили захардкодить в frontmatter каждой статьи массив со ссылками на другие статьи, схожие по тематике.</p><p>Только вместо ссылок мы использовали <strong>slug</strong> постов.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">recommendations: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"collabic"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"vk-feed"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]</span></span></code></pre><p>У нас уже был компонент <strong>PostPreview</strong> для отображения описания поста. Он используется на главной странице. Поэтому мы просто переиспользовали его.</p><p>В компоненте поста, мы получаем список данных о рекомендованных постах:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> recommendations</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> post.recomendations.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">s</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(s));</span></span></code></pre><p>И передаём их в компонент <strong>PostPreview</strong>, который рендерим после тела статьи:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{recommendations.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">r</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">PostPreview</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    key</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.slug}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    title</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.title}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    coverImage</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.coverImage}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    date</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.date}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    slug</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.slug}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    excerpt</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{r.excerpt}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">))}</span></span></code></pre><p>В результате блок рекомендаций выглядит следующим образом:</p><p><img src="https://baranov.guru/assets/posts/post-sharing-and-recommendations/recommendations.webp" alt="Блок рекомендаций" title="Блок рекомендаций"></p><h2 id="social">Добавляем блок "Поделиться в соц. сетях" </h2><p>Для кнопок соц. сетей мы использовали готовое решение - пакет <a href="https://www.npmjs.com/package/react-share">react-share</a>.</p><p>Установим его:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> react-share</span></span></code></pre><p>Далее всё просто, импортим оттуда кнопки и иконки:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { TelegramIcon, TelegramShareButton } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-share"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><p>И передаём в них базовые данные о посте:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> ICON_SIZE</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 40</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">TelegramShareButton</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> url</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{url} </span><span style="color:#6F42C1;--shiki-dark:#B392F0">title</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{post.title}></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">TelegramIcon</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    size</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">ICON_SIZE</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"rounded-3xl grayscale hover:grayscale-0"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">TelegramShareButton</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre><p>С кнопками <strong>соц. сетей</strong> всё просто. Но нам хотелось ещё добавить возможность использовать <strong>нативный механизм "поделиться"</strong> на мобильных устройствах и просто удобно копировать ссылку на статью на десктопах.</p><p>Поэтому мы решил сделать свою кнопку.</p><p>С разметкой там всё просто, поэтому остановлюсь только на интересном, а именно на использовании <strong>navigator.share API</strong>.</p><p>Сначала подготовим данные:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> shareData</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  title: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  text: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  url: post.url,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Затем вызовем <strong>navigator.share</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> handler</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  try</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> navigator.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">share</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(shareData);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="color:#D73A49;--shiki-dark:#F97583">catch</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (e) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // Тут нужен фоллбек на копирование ссылки</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    writeToClipboard</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Нужно учитывать что <strong>navigator.share</strong> может быть недоступен по множеству причин, поэтому я добавил фоллбек на запись ссылки в <strong>clipboard</strong>.</p><p>Но и запись в <strong>clipboard</strong> может быть недоступен по множеству причин, поэтому и это надо учитывать.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> writeToClipboard</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">text</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  try</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> navigator.clipboard.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeText</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(text);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="color:#D73A49;--shiki-dark:#F97583">catch</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (error) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((error </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292E;--shiki-dark:#E1E4E8">).message);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p><img src="https://baranov.guru/assets/posts/post-sharing-and-recommendations/social.webp" alt="Результат" title="Результат"></p><p>А вот так этот блок выглядит при включенном <strong>Ad Block</strong>:</p><p><img src="https://baranov.guru/assets/posts/post-sharing-and-recommendations/blocked.webp" alt="Заблокированный результат" title="Заблокированный результат"></p><p>Не очень конечно, думаем что с блокировщиками рекламы тоже предстоит разобраться со временем.</p><p>На этом всё! 🎉</p><p>В живую результат статьи можно увидеть ниже 🙂</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/post-sharing-and-recommendations/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем Google Analytics в Next.js приложение]]></title>
            <link>https://baranov.guru/posts/nextjs-google-analytics/</link>
            <guid>https://baranov.guru/posts/nextjs-google-analytics</guid>
            <pubDate>Fri, 24 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению Google Analytics в Next.js приложение.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Решили добавить Google Analytics на страницы <a href="https://baranov.guru">блога</a> для базового отслеживания переходов по страницам.</p><p>Если коротко, то есть очень много способов сделать это, в этой статье мы остановимся на двух из них:</p><ul><li><a href="#old">Старый способ</a>, который мы использовали раньше;</li><li><a href="#new">Новый способ</a>, появившийся не так давно;</li></ul><h2 id="old">Старый способ </h2><p>Ранее мы добавляли <strong>аналитику от гугла</strong> следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Script </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/script"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> GA_MEASUREMENT_ID</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "G-1234567890"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">//Не настоящий</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> GtagContainer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">enabled</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">enabled) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Script</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        src</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://www.googletagmanager.com/gtag/js?id=${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GA_MEASUREMENT_ID</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Script</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"google-analytics"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        {</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">          window.dataLayer = window.dataLayer || [];</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">          function gtag(){dataLayer.push(arguments);}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">          gtag('js', new Date());</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">          gtag('config', '${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GA_MEASUREMENT_ID</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}');</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        `</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> GtagContainer;</span></span></code></pre><h2 id="new">Новый способ </h2><p>Теперь же <strong>Vercel</strong> выпустили пока ещё экспериментальный пакет <a href="https://www.npmjs.com/package/@next/third-parties">@next/third-parties</a>, с которым добавить аналитику становится чуть удобнее.</p><p>Установим его.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> @next/third-parties@latest</span></span></code></pre><p>Далее добавим компонент <strong>GoogleAnalytics</strong> в <strong>RootLayout</strong></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { GoogleAnalytics } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '@next/third-parties/google'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> GA_MEASUREMENT_ID</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "G-1234567890"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">//Не настоящий</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> RootLayout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReactNode</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"en"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>{children}&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GoogleAnalytics</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> gaId</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GA_MEASUREMENT_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Теперь всё должно работать.</p><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-google-analytics/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Автопостинг в VK через RSS feed]]></title>
            <link>https://baranov.guru/posts/vk-feed/</link>
            <guid>https://baranov.guru/posts/vk-feed</guid>
            <pubDate>Tue, 21 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Заметили в настройках сообщества в VK интересную настройку, связанную с фидами. И понеслось.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Заметили в настройках сообщества в <strong>VK</strong> интересную настройку <strong>Импорт RSS</strong>.</p><p>А так как мы недавно <a href="https://baranov.guru/posts/nextjs-rss">добавили RSS фид</a>, то мы сразу же решили её опробовать.</p><p>То как <strong>VK</strong> обрабатывает фид - тайна за семью печатями. В документации мы <strong>не нашли</strong> никаких <strong>подробностей</strong>. Там даже <strong>описания</strong> этой функции нет.</p><p>Настройка просто есть, <strong>кому надо - тот разберётся</strong>.</p><p>Нам надо, мы начали разбираться 🙂</p><h2 id="test">Тестируем настройку </h2><p>Итак, выставляем настройку следующим образом:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/setting-no-article.webp" alt="Настройка импорта RSS" title="Настройка импорта RSS"></p><p>Теперь выкладываем фид и ждём.</p><p>Через несколько минут получаем результат!</p><p><img src="https://baranov.guru/assets/posts/vk-feed/no-article-result.webp" alt="Первый результат" title="Первый результат"></p><p>На этом можно было бы и остановиться, но...</p><p>Что там было про <strong>"Публиковать в виде статьи"</strong>?</p><h2 id="article">Тестируем настройку "Публиковать в виде статьи" </h2><p>Меняем настройки:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/setting.webp" alt="&#x22;Настройка \&#x22;Публиковать в виде статьи\&#x22;&#x22;" title="Настройка &#x22;Публиковать в виде статьи&#x22;"></p><ul><li><a href="https://baranov.guru/posts/nextjs-rss">Генерим фид</a>;</li><li>Загружаем его в бакет;</li><li><a href="https://youtu.be/dQw4w9WgXcQ">Ждём</a>;</li></ul><p>И получаем следующий результат:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/article.webp" alt="Первый результат со статьёй" title="Первый результат со статьёй"></p><p>При этом если кликнуть на статью, то она <strong>пустая внутри</strong>:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/empty-article.webp" alt="Пустая статья" title="Пустая статья"></p><h2 id="content">Добавляем содержимое статьи </h2><p>Статья пустая внутри, потому что я не пишу в <strong>RSS</strong> фид содержимое статьи.</p><p>Это легко исправить. Добавляем в код для генерации фида содержимое:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">allPosts.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Adding rss item for post ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    content: post.content,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">});</span></span></code></pre><p>Снова проверяем результат:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/test.webp" alt="Результат со статьёй" title="Результат со статьёй"></p><p>Кажется что-то пошло не так. А что с содержимым статьи?</p><p><img src="https://baranov.guru/assets/posts/vk-feed/markdown-test.webp" alt="Содержимое статьи в формате markdown" title="Содержимое статьи в формате markdown"></p><p>Становится очевидно что причина в том, что <strong>markdown</strong> не поддерживается</p><p>Значит попробуем добавлять в фид <strong>HTML</strong> разметку.</p><p>Снова меняем код генерации фида:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> allPosts) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Adding VK item for post ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> content</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> markdownToHtml</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.content);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    //..</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    content: content,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Проверяем результат:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/test2.webp" alt="Результат со статьёй в формате HTML" title="Результат со статьёй в формате HTML"></p><p>А что с содержимым статьи?</p><p><img src="https://baranov.guru/assets/posts/vk-feed/html-test.webp" alt="Содержимое статьи в формате HTML" title="Содержимое статьи в формате HTML"></p><p>Видимо <strong>VK</strong> не поддерживает <strong>якорные ссылки</strong>...</p><p>А что ещё он не поддерживает?</p><p>Добавляем ещё больше <strong>разного</strong> содержимого в статью:</p><ul><li>Изображения с <strong>относительным</strong> путём;</li><li>Изображения с <strong>абсолютным</strong> путём;</li><li>Блоки кода;</li></ul><p>Результат:</p><p><img src="https://baranov.guru/assets/posts/vk-feed/test3.webp" alt="Содержимое статьи c картинками и кодом" title="Содержимое статьи c картинками и кодом"></p><h2 id="results">Выводы </h2><p>Выводы:</p><ul><li>Изображения с относительным путём - <strong>работают</strong>;</li><li>Изображения с абсолютным путём - <strong>работают</strong>;</li><li>Блоки кода - <strong>не работают</strong>;</li></ul><p>С этим можно жить 🙂</p><p>На этом всё, примеры автопостинга, вы всегда можете посмотреть в нашем <a href="https://vk.com/baranov_guru">сообществе во Вконтакте</a>! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/vk-feed/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Борьба с грамматическими ошибками в markdown файлах]]></title>
            <link>https://baranov.guru/posts/markdown-spellcheck/</link>
            <guid>https://baranov.guru/posts/markdown-spellcheck</guid>
            <pubDate>Mon, 20 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по использованию LTеX для борьбы с грамматическими ошибками в .md файлах.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Заметили большое количество грамматических, орфографических и пунктуационных ошибок в текстах статей <a href="https://baranov.guru">нашего блога</a>.</p><p>Поняли что с этим надо бороться и начали искать варианты.</p><p>Посты мы пишем в markdown файлах, используя <strong>VS Code</strong>, поэтому начали искать <strong>линтер</strong> с возможностью проверки грамматики, орфографии и пунктуации.</p><h2 id="cspell">Расширение code-spell-checker </h2><p>Ранее мы уже использовали расширение <a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker">code-spell-checker</a> поэтому решили попробовать его и в этот раз.</p><p>Но сразу столкнулись с парой проблем:</p><ul><li>отсутствие поддержки русского языка из коробки;</li><li>отсутствие проверки функций проверки грамматики и пунктуации;</li></ul><p><strong>Первая проблема легко решается</strong> установкой расширения с русским словарём <a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker-russian">russian-code-spell-checker</a>.</p><p>А вот вторая проблема <strong>не решается</strong> в рамках данного решения.</p><h2 id="ltex">LTеX </h2><p>Довольно быстро нашли другое решение - <a href="https://valentjn.github.io/ltex/index.html">LTеX</a>.</p><p>Основные особенности <strong>LTeX</strong>:</p><ul><li>Поддержка <strong>Markdown</strong>, <strong>BibTEX</strong>, <strong>ConTEXt</strong>, <strong>LATEX</strong>, <strong>Org</strong>, <strong>reStructuredText</strong>, <strong>R Sweave</strong>, <strong>XHTML</strong>;</li><li>Работает полностью локально, не делает запросов в интернет;</li><li>Поддержка более <strong>20</strong> языков, в том числе <strong>русского</strong>;</li><li>Поддержка <strong>Quick fix</strong> и подсветка ошибок;</li><li>Поддержка пользовательских словарей;</li><li>Нормальная документация;</li></ul><h2 id="install">Настройка расширения для VS code </h2><p>Для того чтобы включить и использовать LTeX для русского языка в <strong>VS Code</strong> надо:</p><ul><li>Установить <a href="https://marketplace.visualstudio.com/items?itemName=valentjn.vscode-ltex">расширение</a>;</li><li>В настройках расширения, либо в файле <strong>settings.json</strong> установить следующие настройки:</li></ul><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "ltex.language"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru-RU"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "ltex.additionalRules.motherTongue"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru-RU"</span></span></code></pre><h2 id="examples">Примеры использования </h2><p>Вот так выглядит подсветка ошибок в файле:</p><p><img src="https://baranov.guru/assets/posts/markdown-spellcheck/ltex_warnings.webp" alt="Подсветка ошибок в файле" title="Подсветка ошибок в файле"></p><p>Вот так ошибки выглядят при наведении на них:</p><p><img src="https://baranov.guru/assets/posts/markdown-spellcheck/ltex_hover.webp" alt="Ошибки при наведении" title="Ошибки при наведении"></p><p>Вот так выглядит меню <strong>Quick fix</strong>:</p><p><img src="https://baranov.guru/assets/posts/markdown-spellcheck/ltex_quick_fix.webp" alt="Меню Quick fix" title="Меню Quick fix"></p><p>Так же можно добавлять слова и правила в исключения:</p><p><img src="https://baranov.guru/assets/posts/markdown-spellcheck/ltex_exclude.webp" alt="Исключения" title="Исключения"></p><p>При этом генерируются .txt файлы с добавленными словами и исключениями для каждого языка.</p><p>Пример <strong>ltex.dictionary.ru-RU.txt</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>бакет</span></span>
<span class="line"><span>захостить</span></span></code></pre><p>На этом всё! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/markdown-spellcheck/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем JSON-LD разметку к блогу на Next.js]]></title>
            <link>https://baranov.guru/posts/nextjs-json-ld/</link>
            <guid>https://baranov.guru/posts/nextjs-json-ld</guid>
            <pubDate>Sat, 18 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению JSON-LD разметки к блогу на Next.js.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>В <a href="https://baranov.guru/posts/nextjs-yandex-turbo">прошлом посте</a>, мы рассказывали о том как сделать так, чтобы в <strong>выдаче Яндекса</strong> отображалась красивая галерея статей.</p><p>Пришло время сделать выдачу ещё лучше, а также поработать над выдачей в других поисковиках. Сделать это можно добавив на страницы разметку <a href="https://json-ld.org/">JSON-LD</a>.</p><h2 id="what">Что такое JSON-LD? </h2><p><strong>JSON-LD (JSON Lightweight Linked Data format)</strong> — это формат метаданных для поисковых систем о типе контента на каждой странице. В теории, наличие подобной разметки на сайте приводит к более высоким результатам в поисковой выдаче.</p><p><strong>JSON-LD</strong> выглядит так:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"BlogPosting"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "headline"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Добавляем JSON-LD разметку к блогу на Next.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "description"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Инструкция по добавлению JSON-LD разметки к блогу на Next.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "datePublished"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"2024-05-18"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "genre"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Technology"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "author"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Person"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "name"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Алексей Баранов"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "url"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "image"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/assets/posts/nextjs-json-ld/cover.webp"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="why">Для чего нужен JSON-LD? </h2><p>В поисковой выдаче <strong>Яндекса</strong> есть, как минимум, 2 элемента, которые напрямую зависят от <strong>JSON-LD</strong>:</p><ul><li><strong>Навигационные цепочки</strong> - они же хлебные крошки; <img src="https://baranov.guru/assets/posts/nextjs-json-ld/navigation.webp" alt="Навигационные цепочки" title="Навигационные цепочки"></li><li>Сниппет <strong>Вопрос-ответ</strong>; <img src="https://baranov.guru/assets/posts/nextjs-json-ld/faq.webp" alt="Вопрос-ответ" title="Вопрос-ответ"></li></ul><p>С поисковой выдачей <strong>Google</strong> всё ещё интереснее. Они называют такую разметку <strong>Структурированными данными</strong>.</p><p>В документации для разработчиков есть целый <a href="https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=ru">раздел посвящённый <strong>Структурированным данным</strong> и <strong>JSON-LD</strong></a>.</p><p>От <strong>Структурированных данных</strong> зависят такие функции поисковой выдачи как:</p><ul><li><strong>Статья</strong> - название говорит само за себя;</li><li><strong>Строка навигации</strong> - аналог <strong>Навигационных цепочек</strong> Яндекса;</li><li><strong>Карусель</strong> - аналог <a href="https://baranov.guru/posts/nextjs-yandex-turbo">Турбо карусели</a> Яндекса;</li><li><strong>Часто задаваемые вопросы</strong> - аналог <strong>Вопрос-ответ</strong> от Яндекса;</li><li><strong>Товары</strong> - описание товаров и их характеристик;</li><li><strong>Видео</strong> - описание и основные моменты видео;</li><li><strong>Организация</strong> - название, часы работы, телефоны и прочее;</li><li><strong>Мероприятие</strong> - где и когда состоится;</li></ul><p>и многие другие (я насчитал <strong>36 штук</strong>).</p><p>Для начала мне хватит просто <strong>Навигационных цепочек</strong> и теоретического улучшения позиций в выдаче 🙂</p><p>Итак, для добавления <strong>JSON-LD</strong> нам нужно:</p><ul><li><a href="#generate">Сформировать описание в формате <strong>JSON-LD</strong></a>;</li><li><a href="#add">Добавить его на страницу</a>;</li><li><a href="#test">Протестировать что всё работает</a>;</li></ul><p><a href="#result">Ссылка на конечный результат</a>.</p><h2 id="generate">Формирование JSON-LD </h2><p>Как не трудно догадаться из названия, <strong>JSON-LD</strong> - это <strong>JSON</strong> объект, составленный по определённой схеме. Для валидации схемы уже есть npm пакет <a href="https://www.npmjs.com/package/schema-dts">schema-dts</a>.</p><p>Установим его:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> schema-dts</span></span></code></pre><p>Далее создадим объект для поста <a href="https://baranov.guru">этого блога</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { BlogPosting, WithContext } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "schema-dts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> URL_BASE</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://baranov.guru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> HOME_OG_IMAGE_URL</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://baranov.guru/logo.webp"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> blogPosting</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">BlogPosting</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"BlogPosting"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  headline: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  datePublished: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toISOString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  genre: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Technology"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  author: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Person"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    name: post.author.name,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/about`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  publisher: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Organization"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Алексей Баранов. Блог"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    logo: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ImageObject"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">HOME_OG_IMAGE_URL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  image: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">ogImage</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  mainEntityOfPage: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"WebPage"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  inLanguage: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru-RU"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Теперь необходимо сформировать <strong>BreadcrumbList</strong> для <strong>Навигационных цепочек</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { BreadcrumbList, WithContext } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "schema-dts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> URL_BASE</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://baranov.guru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> breadcrumbList</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">BreadcrumbList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"BreadcrumbList"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  itemListElement: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ListItem"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      position: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Посты"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      item: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ListItem"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      position: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      item: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Ну и на сладкое мы можем добавить информацию о видео содержащихся на странице:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { VideoObject, WithContext } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "schema-dts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> videoStructuredData</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">VideoObject</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"VideoObject"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  thumbnailUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://i.ytimg.com/vi/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/hqdefault.jpg`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// Миниатюра видео</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  uploadDate: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toISOString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  contentUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://www.youtube.com/watch?v=${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// Ссылка на видео на Youtube</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  embedUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://www.youtube.com/embed/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// Ссылка на встроенное видео с Youtube</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Объединяем всё это вместе в один объект:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> jsonLd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [blogPosting, breadcrumbList, videoStructuredData];</span></span></code></pre><h2 id="add">Добавление на страницу </h2><p>Для того чтобы добавить разметку на страницу, создадим компонент <strong>JsonLd</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  data</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> JsonLd</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">data</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    type</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"application/ld+json"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    dangerouslySetInnerHTML</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{ __html: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(data) }}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> JsonLd;</span></span></code></pre><p>Далее просто добавляем этот компонент на страницу с постом и передаём в него данные:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> jsonLd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [blogPosting, breadcrumbList, videoStructuredData];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">JsonLd</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> data</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{jsonLd} /></span></span></code></pre><p>Теперь необходимо проверить что у нас всё получилось как надо.</p><h2 id="test">Тестирование </h2><p>Заходим на страницу и смотрим разметку:</p><p><img src="https://baranov.guru/assets/posts/nextjs-json-ld/result.webp" alt="Разметка страницы" title="Разметка страницы"></p><p>Итак, разметка появилась на странице.</p><p>Теперь необходимо проверить что разметка валидная. Для этого у гугла есть <a href="https://search.google.com/test/rich-results">специальный инструмент для проверки</a>.</p><p><img src="https://baranov.guru/assets/posts/nextjs-json-ld/testing-tool.webp" alt="Инструмент для проверки" title="Инструмент для проверки"></p><p>Вбиваем в него <strong>адрес нашей страницы</strong>, если она уже где-то хостится, либо <strong>HTML-разметку</strong> страницы.</p><p><img src="https://baranov.guru/assets/posts/nextjs-json-ld/success.webp" alt="Результат проверки" title="Результат проверки"></p><p>Отлично, на этом всё! 🎉</p><p>Теперь выкладываем страницу и ждём индексации.</p><h2 id="result">Готовое решение </h2><p>Прикладываю полный код решения.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// types.ts</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Author</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  name</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  picture</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  slug</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  title</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  description</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  date</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  coverImage</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  author</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Author</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  excerpt</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  ogImage</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  keywords</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  content</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  preview</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  youtubeId</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// src/lib/jsonLd.ts</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  BlogPosting,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  BreadcrumbList,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  VideoObject,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  WithContext,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "schema-dts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Post } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/interfaces/post"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { HOME_OG_IMAGE_URL, URL_BASE } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostJsonLd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">post</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> blogPosting</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">BlogPosting</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"BlogPosting"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    headline: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    datePublished: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toISOString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    genre: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Technology"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    author: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Person"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      name: post.author.name,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/about`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    publisher: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Organization"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Алексей Баранов. Блог"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      logo: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ImageObject"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        url: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">HOME_OG_IMAGE_URL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    image: [</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">ogImage</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    mainEntityOfPage: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"WebPage"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    inLanguage: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru-RU"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> breadcrumbList</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">BreadcrumbList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"BreadcrumbList"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    itemListElement: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ListItem"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        position: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Посты"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        item: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">URL_BASE</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ListItem"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        position: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (post.youtubeId) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> videoStructuredData</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> WithContext</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">VideoObject</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@context"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://schema.org"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "@type"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"VideoObject"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      name: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      thumbnailUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://i.ytimg.com/vi/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/hqdefault.jpg`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// this is the thumbnail for the video straight from youtube</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      uploadDate: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toISOString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      contentUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://www.youtube.com/watch?v=${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// this is the URL for the video on youtube</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      embedUrl: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://www.youtube.com/embed/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">youtubeId</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// this is the URL for the video embed on youtube</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [blogPosting, breadcrumbList, videoStructuredData];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [blogPosting, breadcrumbList];</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// src/app/posts/[slug]/page.ts</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">params</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Params</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> jsonLd</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostJsonLd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">JsonLd</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> data</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{jsonLd} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>На этом всё. Спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-json-ld/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем Яндекс Турбо-страницы к блогу на Next.js]]></title>
            <link>https://baranov.guru/posts/nextjs-yandex-turbo/</link>
            <guid>https://baranov.guru/posts/nextjs-yandex-turbo</guid>
            <pubDate>Fri, 17 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Для того чтобы красиво отображаться в поисковой выдаче Яндекса решили добавить Яндекс Турбо-страницы к блогу. Вот что из этого вышло.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ 7 февраля 2025 года <strong>Yandex</strong> анонсировал <a href="https://webmaster.yandex.ru/blog/yandex-stops-supporting-turbo-technology">прекращение поддержки Turbo страниц</a>.</p><p>Поэтому содержимое этой статьи <strong>устарело</strong> и оставлено здесь просто для истории.</p></aside><p>Для того чтобы красиво отображаться в поисковой выдаче <strong>Яндекса</strong> решил добавить <strong>Яндекс Турбо-страницы</strong> к <a href="https://baranov.guru">этому блогу</a>.</p><p>Вот так это выглядит:</p><p><img src="https://baranov.guru/assets/posts/nextjs-yandex-turbo/result.webp" alt="Турбо-страницы в поиске" title="Турбо-страницы в поиске"></p><p>Процесс этот простой и проходит в 2 основных шага:</p><ul><li><a href="#generate">Сгенерировать фид</a>;</li><li><a href="#add">Добавить информацию о нём в Яндекс Вебмастер</a>;</li></ul><h2 id="generate">Генерация фида </h2><p>Процесс генерации фида почти ничем не отличается от <a href="https://baranov.guru/posts/nextjs-rss">генерации RSS фида</a>.</p><p>Для того чтобы сгенерировать фид, необходимо:</p><ul><li><a href="#frontmatter">получить список всех постов и их метаданные</a>,</li><li><a href="#turbo">сформировать Tурбофид в специальном формате</a></li><li><a href="#build">делать так при каждой сборке</a></li></ul><h3 id="frontmatter">Получение постов и их метаданных </h3><p>Так как посты у меня хранятся в репозитории проекта в формате <strong>.md</strong>, то для получения всех постов мне просто необходимо:</p><ul><li>получить список файлов в директории с постами,</li><li>пройти по каждому посту и получить метаданные</li></ul><p>Для получения метаданных я воспользовался пакетом <a href="https://www.npmjs.com/package/gray-matter">gray-matter</a></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> gray-matter</span></span></code></pre><p>В результате получился такой код:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "fs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { join } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "path"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> matter </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "gray-matter"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Post } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/interfaces/post"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postsDirectory</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"_posts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> realSlug</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slug.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032F62;--shiki-dark:#DBEDFF">md</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fullPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(postsDirectory, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">realSlug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}.md`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fileContents</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(fullPath, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"utf8"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">data</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> matter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(fileContents);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">data, slug: realSlug, content } </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> slugs</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readdirSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(postsDirectory);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slugs</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(slug))</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // сортируем посты по дате в порядке убывания</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">b</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (a.date </span><span style="color:#D73A49;--shiki-dark:#F97583">></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> b.date </span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> posts;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="turbo">Формирование Турбо фида </h3><p>Для того чтобы добавить <strong>RSS фид</strong> я воспользовался пакетом <a href="https://www.npmjs.com/package/turbo-rss">turbo-rss</a>.</p><p>Устанавливаем его командой:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> turbo-rss</span></span></code></pre><p>Алгоритм следующий:</p><ul><li>сформировать описание фида,</li><li>пройти по списку постов, и сформировать feedItem для каждого поста,</li><li>записать всё в .xml файл</li></ul><p>У меня получился вот такой скрипт:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "fs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "node:path"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { getAllPosts } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "../api"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { markdownToTurboHtml } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "../markdownToHtml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// eslint-disable-next-line @typescript-eslint/no-var-requires</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> TR</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> require</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"turbo-rss"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> generateTurboFeed</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Generating Turbo feed..."</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> allPosts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Found ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">allPosts</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#032F62;--shiki-dark:#9ECBFF">} posts.`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> site_url</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://baranov.guru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feedOptions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    language: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    title: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Baranov.Guru | RSS feed"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    description:</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "Личный блог. Пишу о разработке софта, предпринимательстве и просто об интересных мне вещах"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    id: site_url,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    link: site_url,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    image: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/assets/authors/avatar.webp`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    favicon: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/favicon/favicon.ico`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    copyright: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`All rights reserved ${</span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#032F62;--shiki-dark:#9ECBFF">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">getFullYear</span><span style="color:#032F62;--shiki-dark:#9ECBFF">()</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}, Baranov.Guru`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    generator: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Feed for Node.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    feedLinks: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      rss2: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/rss.xml`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feed</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> TR</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(feedOptions);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> allPosts) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Adding turbo item for post ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> content</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> markdownToTurboHtml</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.content);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">item</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      title: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      id: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      image_url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverImage</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      url: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      author: post.author.name,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      date: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      content: content,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      menu: [</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          link: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"http://alexeybaranov.dev"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          text: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Главная страница"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      ],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feedPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"./out/turbo.xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeFileSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(feedPath, feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">xml</span><span style="color:#24292E;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Rss feed is written to ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">feedPath</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}.`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="build">Автоматизация сборки фида </h3><p>Добавляем в package.json скрипт <strong>postbuild</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "scripts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "build"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"next build"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "postbuild"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"npx tsx ./src/lib/feeds/generate.ts"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Он запустится сразу же после <strong>успешной сборки</strong> проекта и запишет фид в папку с результатами сборки.</p><h2 id="add">Добавление информации о фиде в Яндекс Вебмастер </h2><p>Теперь осталось только добавить информацию о фиде в поисковую машину <strong>Яндекса</strong>.</p><p>Заходим в <a href="https://webmaster.yandex.ru">Яндекс Вебмастер</a>.</p><p>Переходим на вкладку <strong>Турбо-страницы для контентных сайтов</strong> --> <strong>Источники</strong>.</p><p><img src="https://baranov.guru/assets/posts/nextjs-yandex-turbo/webmaster.webp" alt="Добавление через Вебмастер" title="Добавление через Вебмастер"></p><p>Добавляем фид удобным нам способом:</p><ul><li>Руками, через форму;</li><li>Через <strong>API</strong>;</li></ul><p>Добавить фид надо всего 1 раз, поэтому я не стал заморачиваться с API и просто добавил ссылку на фид руками.</p><p>Всё готово! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-yandex-turbo/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Подключение счётчика Яндекс Метрики к Next.js приложению]]></title>
            <link>https://baranov.guru/posts/nextjs-yandex-metrika/</link>
            <guid>https://baranov.guru/posts/nextjs-yandex-metrika</guid>
            <pubDate>Wed, 15 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению счётчика Яндекс Метрики к Next.js блогу.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Одним из важнейших аспектов поддержки любого сайта является работа с аналитикой.</p><p>Аналитические <strong>счётчики</strong> для сайтов играют ключевую роль в понимании поведения посетителей и оптимизации веб-ресурсов. Они позволяют владельцам сайтов собирать ценные <strong>данные о посетителях</strong>, <strong>источниках переходов</strong>, <strong>поведении пользователей</strong> на страницах сайта и многом другом.</p><p>В этой статье мы расскажем о нескольких способах подключения счётчика <strong>Яндекс Метрики</strong> к <strong>Next.js</strong> приложению.</p><p>Однако, все эти способы <strong>очень похожи</strong> и для того чтобы лучше их понять и грамотнее их использовать, начнём с написания <strong>собственного решения</strong>.</p><h2 id="own-solution">Написание собственного решения </h2><p>Итак, нам нужно:</p><ul><li><a href="#own-solution-create-component">создать компонент, который будет загружать счётчик на страницу</a>,</li><li><a href="#own-solution-add-to-layout">добавить этот компонент в Root Layout</a>,</li><li><a href="#own-solution-change-route">научиться обрабатывать события перехода между страницами</a>,</li><li><a href="#own-solution-handle-events">а также научиться прокидывать любые другие события в счётчик</a></li></ul><h3 id="own-solution-create-component">Создание компонента </h3><p>Итак, наш компонент должен:</p><ul><li>Загрузить скрипт <strong>Яндекс Метрики</strong> с указанными настройками;</li><li>Предоставить методы для работы с <strong>API Яндекс Метрики</strong>;</li></ul><h4>Загрузка скрипта</h4><p>Добавим компонент <strong>YandexMetrikaInitializer</strong>.</p><p>Он будет принимать <strong>id</strong> счётчика и <a href="#own-solution-init-params">набор параметров счётчика</a> и рендерить <strong>Script</strong>-компонент, с кодом загрузки счётчика метрики с указанными параметрами.</p><p>Это тот самый код, который можно получить в <a href="https://metrika.yandex.ru/settings">личном кабинете Яндекс Метрики</a> при создании счётчика.</p><p>В итоге должно получиться нечто похожее:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Script </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/script"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YandexMetrikaInitParameters } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./types"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  initParameters</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaInitParameters</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaInitializer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">initParameters</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">  /* eslint-disable @next/next/no-img-element */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Script</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"text/javascript"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`ym_${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">id</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        {</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`(function (m, e, t, r, i, k, a) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  m[i] =</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    m[i] ||</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    function () {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      (m[i].a = m[i].a || []).push(arguments);</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    };</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  m[i].l = 1 * new Date();</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  for (var j = 0; j &#x3C; document.scripts.length; j++) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    if (document.scripts[j].src === r) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      return;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    }</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  }</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">  (k = e.createElement(t)),</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    (a = e.getElementsByTagName(t)[0]),</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    (k.async = 1),</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    (k.src = r),</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    a.parentNode.insertBefore(k, a);</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">ym(${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">id</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}, "init", ${</span><span style="color:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="color:#032F62;--shiki-dark:#9ECBFF">(</span><span style="color:#24292E;--shiki-dark:#E1E4E8">initParameters</span><span style="color:#032F62;--shiki-dark:#9ECBFF">)</span><span style="color:#032F62;--shiki-dark:#9ECBFF">});`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">noscript</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">img</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">            src</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`https://mc.yandex.ru/watch/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">id</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">            style</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{ position: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"absolute"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, left: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"-9999px;"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">            alt</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">          /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">noscript</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> MyYandexMetrikaInitializer;</span></span></code></pre><h4 id="own-solution-init-params">Набор параметров счётчика </h4><p>Вот список параметров счётчика, взятый из <a href="https://yandex.ru/support/metrica/code/counter-initialize.html?lang=ru">официальной доки</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaInitParameters</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  accurateTrackBounce</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  childIframe</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  clickmap</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  defer</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  ecommerce</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  params</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#D73A49;--shiki-dark:#F97583"> |</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  userParams</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  trackHash</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  trackLinks</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  trustedDomains</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[];</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  type</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  webvisor</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  triggerEvent</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  sendTitle</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><h3 id="own-solution-add-to-layout">Добавление компонента в Root Layout </h3><p>Теперь нам надо добавить <a href="#own-solution-create-component">наш компонент</a> в <strong>Root Layout</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> RootLayout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Readonly</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReactNode</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}>) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaInitializer</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">          id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{id}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">          initParameters</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{ webvisor: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, defer: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Обратите внимание, что инициализировать компонент лучше внутри тега <strong>head</strong>.</p><h3 id="own-solution-change-route">Обработка перехода между страницами </h3><p>При переходе между страницами <strong>Next.js</strong> не всегда перерендеривает весь <strong>Layout</strong>.</p><p>Это одна из встроенных в фреймворк <strong>оптимизаций</strong>, она позволяет <strong>снизить время перехода</strong> между страницами приложения.</p><p>Однако нам сейчас это поведение <strong>вредит и не позволяет</strong> полноценно отслеживать переходы посетителя между страницами.</p><p>Как это победить? Надо просто подписаться на <strong>события изменения URL</strong>. Сделать это можно используя хуки <strong>usePathname</strong> и <strong>useSearchParams</strong> встроенного пакета <strong>next/navigation</strong>, а также воспользовавшись <strong>react-хуком useEffect</strong>.</p><p>Обернём наш компонент для инициализации в контейнер. Назовём его <strong>YandexMetrikaContainer</strong> и подпишемся на изменения <strong>URL</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { usePathname, useSearchParams } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/navigation"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React, { useEffect } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YM_COUNTER_ID } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaInitializer </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./YandexMetrikaInitializer"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaContainer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">enabled</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pathname</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> usePathname</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> search</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useSearchParams</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  useEffect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      `${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">pathname</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">search</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">size</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `?${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">search</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ""}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">window</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">location</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">hash</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }, [hit, pathname, search]);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">enabled) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaInitializer</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YM_COUNTER_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      initParameters</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{ webvisor: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, defer: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaContainer;</span></span></code></pre><p>Отлично, теперь мы можем видеть в консоли сообщение о том что <strong>адрес страницы изменился</strong>. Но как передать эту информацию в счётчик?</p><p>Для этого нужно воспользоваться одним из встроенных методов <strong>Яндекс Метрики</strong> под названием <a href="https://yandex.ru/support/metrica/objects/hit.html"><strong>hit</strong></a>.</p><p>Для удобства, давайте обернём вызов этого метода в хук и назовём его <strong>useYandexMetrika</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YandexMetrikaHitOptions, YandexMetrikaMethod } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./types"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">declare</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> ym</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  method</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaMethod</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  ...</span><span style="color:#E36209;--shiki-dark:#FFAB70">params</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> void</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> enabled</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> !!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="color:#D73A49;--shiki-dark:#F97583"> ===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "production"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useYandexMetrika</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaHitOptions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (enabled) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(id, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"hit"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url, options);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`%c[YandexMetrika](hit)`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`color: orange`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { hit };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> useYandexMetrika;</span></span></code></pre><p>Вот так выглядит описание типов <strong>YandexMetrikaHitOptions</strong> и <strong>YandexMetrikaHitParams</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaHitOptions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  callback</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> void</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  ctx</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  params</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaHitParams</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  referer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  title</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaHitParams</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  order_price</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  currency</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Теперь используем наш хук при переходе между страницами, должно получиться примерно так:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { usePathname, useSearchParams } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/navigation"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React, { useEffect } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YM_COUNTER_ID } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./constants"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> useYandexMetrika </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./useYandexMetrika"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaInitializer </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./YandexMetrikaInitializer"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaContainer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">enabled</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pathname</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> usePathname</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> search</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useSearchParams</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useYandexMetrika</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YM_COUNTER_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  useEffect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">pathname</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">search</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">size</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `?${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">search</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ""}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">window</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">location</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">hash</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }, [hit, pathname, search]);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">enabled) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaInitializer</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      id</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YM_COUNTER_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      initParameters</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{ webvisor: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, defer: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaContainer;</span></span></code></pre><p><strong>Важно!</strong> Если вы используете серверный рендеринг, не забудьте обернуть <strong>YandexMetrikaContainer</strong> в <strong>Suspense-тег</strong> для того чтобы не сломать его.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Suspense } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Suspense</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaContainer</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enabled</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Suspense</span><span style="color:#24292E;--shiki-dark:#E1E4E8">>;</span></span></code></pre><h3 id="own-solution-handle-events">Отправка событий в счётчик </h3><p>Отправить любое другое событие в счётчик можно похожим образом.</p><p>Просто расширим хук <strong>useYandexMetrika</strong> новыми методами, например для использования события <a href="https://yandex.ru/support/metrica/objects/reachgoal.html"><strong>reachGoal</strong></a>, можно добавить следующий метод:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> reachGoal</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  target</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  params</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  callback</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> void</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  ctx</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (enabled) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(id, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"reachGoal"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, target, params, callback, ctx);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`%c[YandexMetrika](reachGoal)`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`color: orange`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, target);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>В итоге код нашего хука будет выглядеть следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YandexMetrikaHitOptions, YandexMetrikaMethod } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "./types"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">declare</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> ym</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  method</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaMethod</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  ...</span><span style="color:#E36209;--shiki-dark:#FFAB70">params</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> void</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> enabled</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> !!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="color:#D73A49;--shiki-dark:#F97583"> ===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "production"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useYandexMetrika</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">id</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaHitOptions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (enabled) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(id, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"hit"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url, options);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`%c[YandexMetrika](hit)`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`color: orange`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> reachGoal</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    target</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    params</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    callback</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#005CC5;--shiki-dark:#79B8FF"> void</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">    ctx</span><span style="color:#D73A49;--shiki-dark:#F97583">?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> unknown</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (enabled) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(id, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"reachGoal"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, target, params, callback, ctx);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    } </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`%c[YandexMetrika](reachGoal)`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`color: orange`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, target);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { hit, reachGoal };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> useYandexMetrika;</span></span></code></pre><p>Использовать <a href="https://yandex.ru/support/metrica/objects/method-reference.html">другие методы</a> можно аналогичным образом.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaMethod</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "init"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "hit"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "addFileExtension"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "extLink"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "file"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "firstPartyParams"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "firstPartyParamsHashed"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "getClientID"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "notBounce"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "params"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "reachGoal"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "setUserID"</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  |</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "userParams"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><p>На этом всё, по большому счёту, для использования <strong>Яндекс Метрики</strong> в <strong>Next.js</strong> приложении больше ничего делать не надо.</p><h2 id="react-yandex-metrika">Использование пакета react-yandex-metrika </h2><p>Можно написать своё решение, а можно воспользоваться одним из уже готовых пакетов, например <a href="https://www.npmjs.com/package/react-yandex-metrika"><strong>react-yandex-metrika</strong></a>.</p><p>Итак, для этого нам нужно:</p><ul><li><a href="#rym-create-component">создать компонент, который будет загружать счётчик и передавать ему события</a>,</li><li><a href="#rym-subscribe">подписаться на события перехода между страницами</a>,</li><li><a href="#rym-add-to-layout">добавить компонент в Root Layout</a>,</li><li><a href="#rym-dev">не прокидывать события при локальной отладке страниц</a></li></ul><h3 id="rym-create-component">Создание компонента </h3><p>Сначала установим пакет:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> react-yandex-metrika</span></span></code></pre><p>Далее нам необходимо создать компонент, назовём его <strong>YandexMetrikaContainer</strong>.</p><p>Так как счётчик должен быть <a href="https://nextjs.org/docs/app/building-your-application/rendering/client-components">Клиентским компонентом</a>, то в файл с компонентом, перед всеми импортами, необходимо добавить директиву <a href="https://react.dev/reference/rsc/use-client">"use client"</a>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span></code></pre><p>Далее создаём простой функциональный компонент, использующий <strong>YMInitializer</strong> из пакета <strong>react-yandex-metrika</strong>.</p><p>Прокидываем в него номер счётчика и другие необходимые нам настройки счётчика.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { YMInitializer } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-yandex-metrika"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> YM_COUNTER_ID</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 12345678</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">//Не настоящий :)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaContainer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YMInitializer</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      accounts</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YM_COUNTER_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      options</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        defer: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        webvisor: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        clickmap: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        trackLinks: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        accurateTrackBounce: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      version</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"2"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaContainer;</span></span></code></pre><p>Но это ещё не всё, помимо загрузки счётчика, нам нужно отправить в него событие попадания на страницу.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ym </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-yandex-metrika"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"hit"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">useEffect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(window.location.pathname </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> window.location.search);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}, []);</span></span></code></pre><p>На этом бы можно было остановиться, но при переходе между страницами <strong>Next.js</strong> не рендерит весь <strong>Root Layout</strong>, поэтому событие будет вызвано только 1 раз.</p><p>Что бы исправить ситуацию, нам нужно подписаться на событие перехода между страницами.</p><h3 id="rym-subscribe">Подписка на события перехода по страницам </h3><p><strong>Next.js</strong> Содержит в себе пакет <strong>Router</strong>, который позволяет подписаться на нужное нам событие.</p><p>Выглядит это следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Router </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/router"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">Router.events.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"routeChangeComplete"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, callback);</span></span></code></pre><p>В итоге у нас должно получиться вот так:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Router </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/router"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ym </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-yandex-metrika"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"hit"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">useEffect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(window.location.pathname </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> window.location.search);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  Router.events.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"routeChangeComplete"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(url));</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}, []);</span></span></code></pre><p>Теперь настало время добавить компонент в <strong>Root Layout</strong>.</p><h3 id="rym-add-to-layout">Добавление в Root Layout </h3><p>Тут всё довольно просто, я решил добавить компонент после закрывающего тега <strong>&#x3C;/body></strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> RootLayout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Readonly</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReactNode</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}>) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"scroll-smooth"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{inter.className}></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">/></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="rym-dev">Работа в Dev режиме </h3><p>Также мне не нужно чтобы счётчик срабатывал когда я запускаю приложение в режиме отладки, поэтому неплохо бы добавить какой-то признак того что мы в режиме отладки</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> analyticsEnabled</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> !!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="color:#D73A49;--shiki-dark:#F97583"> ===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "production"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span></code></pre><p>В итоге использование компонента будет выглядеть следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaContainer</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{analyticsEnabled} /></span></span></code></pre><h3 id="rym-final">Итоговый результат </h3><p>Итоговый код компонента <strong>YandexMetrikaContainer</strong></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">"use client"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Router </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "next/router"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React, { useCallback, useEffect } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ym, { YMInitializer } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "react-yandex-metrika"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Props</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> boolean</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> YM_COUNTER_ID</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 12345678</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">//Не настоящий :)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> YandexMetrikaContainer</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">FC</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">Props</span><span style="color:#24292E;--shiki-dark:#E1E4E8">> </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ({ </span><span style="color:#E36209;--shiki-dark:#FFAB70">enabled</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> hit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> useCallback</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">      if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (enabled) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">        ym</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"hit"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      } </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`%c[YandexMetrika](HIT)`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`color: orange`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, url);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    [enabled],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  useEffect</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(() </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(window.location.pathname </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> window.location.search);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    Router.events.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"routeChangeComplete"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, (</span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(url));</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }, [hit]);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">enabled) </span><span style="color:#D73A49;--shiki-dark:#F97583">return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YMInitializer</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      accounts</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{[</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YM_COUNTER_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      options</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{{</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        defer: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        webvisor: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        clickmap: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        trackLinks: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        accurateTrackBounce: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      }}</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">      version</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"2"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> YandexMetrikaContainer;</span></span></code></pre><p>Итоговый код компонента <strong>Root Layout</strong></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> analyticsEnabled</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> !!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">NODE_ENV</span><span style="color:#D73A49;--shiki-dark:#F97583"> ===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "production"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> RootLayout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Readonly</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;{</span></span>
<span class="line"><span style="color:#E36209;--shiki-dark:#FFAB70">  children</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> React</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">ReactNode</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}>) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"scroll-smooth"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> className</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{inter.className}></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        ...</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      &#x3C;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">YandexMetrikaContainer</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> enabled</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8">{analyticsEnabled} /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    &#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="alternatives">Альтернативные решения </h2><p>Вот ещё пара похожих пакетов <a href="https://www.npmjs.com/package/react-metrika">react-metrika</a> и <a href="https://www.npmjs.com/package/next-yandex-metrika">next-yandex-metrika</a>.</p><p>Все они делают примерно одно и то же. Подробно останавливаться на них не вижу смысла.</p><p>На этом всё, спасибо за внимание! 🎉</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-yandex-metrika/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Добавляем RSS-фид к статическому Next.js приложению]]></title>
            <link>https://baranov.guru/posts/nextjs-rss/</link>
            <guid>https://baranov.guru/posts/nextjs-rss</guid>
            <pubDate>Tue, 14 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция по добавлению RSS фида к статическому Next.js приложению.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Так как формат <strong>RSS фидов</strong> ещё жив, решили добавить фид и для <a href="https://baranov.guru">этого блога</a>.</p><p>Процесс этот простой и проходит в 2 основных шага:</p><ul><li><a href="#generate">Сгенерировать фид</a>;</li><li><a href="#add">Добавить информацию о нём на страницы сайта</a>;</li></ul><h2 id="generate">Генерация RSS фида </h2><p>Для того чтобы сгенерировать фид, необходимо:</p><ul><li><a href="#frontmatter">получить список всех постов и их метаданные</a>,</li><li><a href="#xml">сформировать RSS фид в формате .XML</a></li><li><a href="#build">делать так при каждой сборке</a></li></ul><h3 id="frontmatter">Получение постов и их метаданных </h3><p>Так как посты у нас хранятся в репозитории проекта в формате <strong>.md</strong>, то для получения всех постов нам просто необходимо:</p><ul><li>получить <strong>список файлов</strong> в директории с постами,</li><li>пройти по каждому посту и получить <strong>метаданные</strong></li></ul><p>Для получения метаданных я воспользовался пакетом <a href="https://www.npmjs.com/package/gray-matter">gray-matter</a></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> gray-matter</span></span></code></pre><p>В результате получился такой код:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "fs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { join } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "path"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> matter </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "gray-matter"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Post } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "@/interfaces/post"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postsDirectory</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(process.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"_posts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> realSlug</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slug.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032F62;--shiki-dark:#DBEDFF">md</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fullPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(postsDirectory, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">realSlug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}.md`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fileContents</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readFileSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(fullPath, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"utf8"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">data</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> matter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(fileContents);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">data, slug: realSlug, content } </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">()</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> slugs</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">readdirSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(postsDirectory);</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slugs</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostBySlug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(slug))</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">    // сортируем посты по дате в порядке убывания</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    .</span><span style="color:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">b</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> (a.date </span><span style="color:#D73A49;--shiki-dark:#F97583">></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> b.date </span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> posts;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="xml">Формирование .XML фида </h3><p>Для того чтобы добавить <strong>RSS фид</strong> мы воспользовались пакетом <a href="https://www.npmjs.com/package/feed">feed</a>.</p><p>Устанавливаем его командой:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> i</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> feed</span></span></code></pre><p>Алгоритм следующий:</p><ul><li>сформировать описание фида,</li><li>пройти по списку постов, и сформировать feedItem для каждого поста,</li><li>записать всё в .xml файл</li></ul><p>У нас получился вот такой скрипт:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> fs </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "fs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Feed } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "feed"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "node:path"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { getAllPosts } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "../api"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> default</span><span style="color:#D73A49;--shiki-dark:#F97583"> async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> generateRssFeed</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Generating RSS feed..."</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> allPosts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getAllPosts</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Found ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">allPosts</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#032F62;--shiki-dark:#9ECBFF">} posts.`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> site_url</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> "https://baranov.guru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feedOptions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    language: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"ru"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    title: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Baranov.Guru | RSS feed"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    description:</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "Личный блог. Пишу о разработке софта, предпринимательстве и просто об интересных мне вещах"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    id: site_url,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    link: site_url,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    image: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/assets/authors/avatar.webp`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    favicon: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/favicon/favicon.ico`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    copyright: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`All rights reserved ${</span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#032F62;--shiki-dark:#9ECBFF">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">getFullYear</span><span style="color:#032F62;--shiki-dark:#9ECBFF">()</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}, Baranov.Guru`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    generator: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Feed for Node.js"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    feedLinks: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      rss2: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/rss.xml`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  };</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feed</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Feed</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(feedOptions);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  allPosts.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="color:#24292E;--shiki-dark:#E1E4E8">((</span><span style="color:#E36209;--shiki-dark:#FFAB70">post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) </span><span style="color:#D73A49;--shiki-dark:#F97583">=></span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Adding rss item for post ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      title: post.title,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      id: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      link: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}/posts/${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      image: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">site_url</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">post</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">coverImage</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      author: [{ name: post.author.name }],</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      description: post.description,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      date: </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Date</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(post.date),</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">  const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> feedPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"./out/feed.xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  fs.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeFileSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(feedPath, feed.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">rss2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Rss feed is written to ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">feedPath</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}.`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><h3 id="build">Автоматизация сборки фида </h3><p>Добавляем в package.json скрипт <strong>postbuild</strong>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">  "scripts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "build"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"next build"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">    "postbuild"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"npx tsx ./src/lib/feeds/generate.ts"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Он запустится сразу же после <strong>успешной сборки</strong> проекта и запишет фид в папку с результатами сборки.</p><h2 id="add">Добавление информации о фиде на страницы сайта </h2><p>Теперь осталось только добавить информацию о фиде на страницы сайта, для того чтобы различные расширения и боты могли легко найти его и отобразить для пользователей.</p><p>В файл с <strong>Root Layout</strong> нашего приложения добавляем следующий код:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> metadata</span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Metadata</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  alternates: {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    types: {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">      "application/rss+xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/feed.xml"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span></code></pre><p>Если вы всё сделали правильно, то в <strong>&#x3C;head></strong> элементе вашей страницы появится следующая строчка:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-tsx"><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  &#x3C;</span><span style="color:#22863A;--shiki-dark:#85E89D">link</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    rel</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"alternate"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    type</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"application/rss+xml"</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">    href</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https://baranov.guru/feed.xml"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  /></span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&#x3C;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">></span></span></code></pre><p>Всё готово! 🎉</p><h2>P.S.</h2><p>Вот ссылка на наш <a href="https://baranov.guru/feed.xml">RSS фид</a>.</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/nextjs-rss/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Next.js + Yandex Object Storage = Не работают ссылки]]></title>
            <link>https://baranov.guru/posts/yandex-object-storage-nextjs/</link>
            <guid>https://baranov.guru/posts/yandex-object-storage-nextjs</guid>
            <pubDate>Mon, 13 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Очень часто при попытке хостить Next.js приложение использующее App Router в S3 бакете можно столкнуться с тем что страницы приложения не открываются при заходе по прямой ссылке.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Размещая <a href="https://baranov.guru">этот блог</a> в <a href="https://baranov.guru/posts/yandex-object-storage"><strong>Yandex Object Storage</strong></a> мы столкнулись с некоторыми проблемами.</p><p>В этой статье мы расскажем об одной из них.</p><h2 id="problem">Проблема </h2><p>Очень часто при попытке хостить <strong>Next.js</strong> приложение использующее <strong>App Router</strong> в <strong>S3 бакете</strong> можно столкнуться с тем что:</p><ul><li><strong>Страницы приложения не открываются при заходе по прямой ссылке</strong>,</li><li>при этом при переходе через нажатие <strong>по внутренней ссылке всё работает как надо</strong>.</li></ul><p>На первый взгляд такое поведение вводит в ступор. Но у этой ситуации есть две вполне логичные причины.</p><h2 id="first">Причина 1 </h2><p>По умолчанию для страницы <strong>/about</strong> генерируются страницы вида <strong>/about.html</strong>.</p><p>Такой формат файлов не подходит для того чтобы хоститься в <strong>S3 бакете</strong> по той причине что URL-адрес <strong>example.com/about</strong> будет всегда отвечать ошибкой <strong>404</strong>.</p><p>Происходит это потому что в бакете <strong>нет объекта с таким именем</strong>.</p><p>Для того чтобы получить этот объект URL-адрес должен содержать ещё и расширение файла (<strong>example.com/about.html</strong>)</p><h2 id="second">Причина 2 </h2><p>По умолчанию <strong>Next.js</strong> перенаправляет URL-адреса с косой чертой в конце на их аналог без косой черты в конце.</p><p>Например, <strong>/about/</strong> будет перенаправляться на <strong>/about</strong>.</p><p>Нам нужно настроить это поведение так, чтобы оно действовало <strong>противоположным образом</strong>:</p><p>URL-адреса без завершающих косых черт <strong>должны перенаправляться</strong> на их аналоги <strong>с завершающими косыми чертами</strong>.</p><h2 id="solution">Решение </h2><p>Чтобы изменить поведение по умолчанию, нужно изменить пару параметров в файле <strong>next.config.js</strong>.</p><p>Добавляем настройку <em><strong>trailingSlash</strong></em> со значением <em><strong>true</strong></em>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  trailingSlash</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span></code></pre><p>И проверяем что значением поля <em><strong>output</strong></em> является <em><strong>export</strong></em>.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-javascript"><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">  output</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"export"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span></code></pre><p>Итоговый конфиг должен выглядеть следующим образом:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">/** </span><span style="color:#D73A49;--shiki-dark:#F97583">@type</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> {import('next').NextConfig}</span><span style="color:#6A737D;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nextConfig</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  output: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"export"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  trailingSlash: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">module</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">exports</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> nextConfig;</span></span></code></pre><p>При такой конфигурации страница <strong>/about</strong> генерит файл <strong>/about/index.html</strong> (вместо <strong>/about.html</strong> по умолчанию).</p><p>В таком случае при обращении по адресу <strong>example.com/about</strong>:</p><ul><li><strong>Next.js</strong> перенаправит нас по адресу <strong>example.com/about/</strong></li><li>Бакет при обращении по адресу <strong>example.com/about/</strong> отдаст нам файл <strong>/about/index.html</strong></li></ul><p>И мы увидим искомую страницу! 🎉</p><p>На этом всё. Спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/yandex-object-storage-nextjs/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Хостинг в S3 от Яндекса]]></title>
            <link>https://baranov.guru/posts/yandex-object-storage/</link>
            <guid>https://baranov.guru/posts/yandex-object-storage</guid>
            <pubDate>Sun, 12 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Инструкция как использовать Yandex Object Storage для хостинга статического сайта.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>Сегодня мы расскажем как использовать сервис <strong>Yandex Object Storage</strong> для хостинга статических сайтов. Итак, для того чтобы начать, нам необходимо:</p><ul><li><p>Зайти в облачную <a href="https://console.yandex.cloud/link/yandexgpt">консоль управления Yandex Cloud</a>.</p></li><li><p>Авторизоваться или, если у вас ещё нет аккаунта, зарегистрироваться.</p></li><li><p>Среди списка сервисов выбрать и перейти на страницу <a href="https://console.yandex.cloud/link/storage">Object Storage</a>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/services.webp" alt="Список сервисов" title="Список сервисов"></p></li></ul><p>Сервис <strong>Object Storage</strong> — это универсальный и масштабируемый сервис для хранения данных, подходящий как для высоконагруженных систем с быстрым доступом к информации, так и для менее требовательных проектов. С его помощью можно хранить данные в виде объектов с текстовым идентификатором, использовать разные классы хранилищ, управлять жизненным циклом объектов, работать с большими объектами размером в несколько терабайт и публиковать статические веб-сайты. HTTP API сервиса совместим с <strong>API Amazon S3</strong>, что позволяет использовать разнообразные инструменты для работы с объектными хранилищами. В этой статье мы как раз остановимся на публикации статических сайтов.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/overview.webp" alt="Yandex Object Storage" title="Yandex Object Storage"></p><p>Для того чтобы захостить сайт необходимо сделать 3 вещи:</p><ul><li><a href="#create-bucket">Создать бакет и загрузить в него файлы сайта</a>;</li><li><a href="#connect-domain">Подключить к бакету свой домен</a>;</li><li><a href="#add-https">Настроить https подключение</a>;</li></ul><h2 id="create-bucket">Создание бакета </h2><p>Итак, для того чтобы разместить свой статический сайт в бакете <strong>Object Storage</strong> необходимо:</p><p>Нажать <strong>создать бакет</strong>.</p><p>Бакет - это логическая сущность, которая помогает организовать хранение объектов.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create.webp" alt="Создание бакета" title="Создание бакета"></p><p>Загрузить файлы с сайтом в бакет одним из предложенных способов.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/upload.webp" alt="Загрузка объектов" title="Загрузка объектов"></p><p>После загрузки должно получиться что-то в этом роде.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/objects.webp" alt="Загруженные объекты" title="Загруженные объекты"></p><h2 id="connect-domain">Подключение своего домена </h2><p>Далее переходим на вкладку <strong>Веб-сайт</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/site-tab.webp" alt="Вкладка Веб-сайт" title="Вкладка Веб-сайт"></p><p>Если у вас ещё нет доменов в <strong>Cloud DNS</strong>, то система предложит вам создать их.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/empty-dns.webp" alt="Пустые домены" title="Пустые домены"></p><p>Нажимаем <strong>создать запись</strong> и попадаем на страницу создания записи.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-dns.webp" alt="Создание записи" title="Создание записи"></p><p>Нажимаем <strong>создать зону</strong> и создаём зону DNS.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-zone.webp" alt="Создание зоны" title="Создание зоны"></p><p>После этого нажимаем <strong>сохранить</strong> и создаём ресурсную запись в этой зоне.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-dns-record.webp" alt="Создание ресурсной записи" title="Создание ресурсной записи"></p><p>Добавляем указатель на файл с главной страницей сайта и нажимаем <strong>сохранить</strong>.</p><p>В итоге должно получиться как-то так.</p><h2 id="add-https">Подключение HTTPS </h2><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/site2.webp" alt="Результат создания домена" title="Результат создания домена"></p><p>Далее переходим на вкладку <strong>HTTPS</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/https-tab.webp" alt="Вкладка HTTPS" title="Вкладка HTTPS"></p><p>Видим сообщение о том что у нас ещё нет конфигурации <strong>HTTPS</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/empty-https.webp" alt="Нет конфигурации HTTPS" title="Нет конфигурации HTTPS"></p><p>Нажимаем <strong>настроить</strong> и попадаем на выбор источника сертификата.</p><p>Если у вас есть свой сертификат, то вы можете загрузить его нажав на вкладку <strong>свой сертификат</strong>. Если же нет, то вы можете воспользоваться сервисом <strong>Certificate Manager</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/empty-certificate.webp" alt="Выбор источника сертификата" title="Выбор источника сертификата"></p><p>Переходим в <strong>Certificate Manager</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/add-certificate-overview.webp" alt="Certificate Manager" title="Certificate Manager"></p><p>Нажимаем <strong>добавить сертификат</strong>. Выбираем опцию <strong>Сертификат от Let's Encrypt</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-certificate.webp" alt="Добавление сертификата" title="Добавление сертификата"></p><p>Заполняем необходимые данные для сертификата и нажимаем <strong>создать</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-certificate-2.webp" alt="Заполненный сертификат" title="Заполненный сертификат"></p><p>Далее заходим в сертификат.</p><p>И видим что для проверки прав на домен необходимо создать ресурсные записи <strong>CNAME</strong> либо <strong>TXT</strong> со специальным кодом проверки.</p><p>Нажимаем <strong>Создать запись</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-certificate-records.webp" alt="Проверка прав на домены" title="Проверка прав на домены"></p><p>Так как мы уже создали зону DNS, то все данные подтянутся автоматически. Нам лишь остаётся нажать на кнопку <strong>Создать</strong>.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/create-certificate-records-2.webp" alt="Создание ресурсной записи" title="Создание ресурсной записи"></p><p>После этого придётся подождать пока произойдёт проверка валидности записей, и сертификат будет выпущен.</p><p>Обычно этот процесс занимает <strong>15-20 минут</strong>, но теоретически может идти и до <strong>суток</strong>.</p><p>Возвращаемся на вкладку <strong>HTTPS</strong> в настройках нашего бакета.</p><p>И выбираем созданный сертификат.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/select-certificate.webp" alt="Выбор сертификата" title="Выбор сертификата"></p><p>Должно получиться как-то так.</p><p><img src="https://baranov.guru/assets/posts/yandex-object-storage/certificate-added.webp" alt="Результат добавления сертификата" title="Результат добавления сертификата"></p><p>Итак, если вы всё настроили правильно, то через пару минут ваш сайт станет доступен по вашему доменному имени по протоколу <strong>HTTPS</strong>! 🎉</p><p>На этом всё, спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/yandex-object-storage/cover.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Начинаем работу с YandexGPT API]]></title>
            <link>https://baranov.guru/posts/yandex-gpt-intro/</link>
            <guid>https://baranov.guru/posts/yandex-gpt-intro</guid>
            <pubDate>Fri, 03 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Рассказываем и показываем как использовать генеративные модели Яндекса через YandexGPT API.]]></description>
            <content:encoded><![CDATA[<aside class="post-disclaimer" role="note"><p>⚠️ Изначально статья была опубликована в 2024 году, в старом блоге проекта по адресу <strong>alexeybaranov.dev</strong>.</p><p>Данные в статье могли устареть. Если вы нашли ошибку, пожалуйста <a href="mailto:hello@baranov.guru">напишите нам</a>.</p></aside><p>В одном из прошлых постов мы рассказывали про сервис 300 от Яндекса и его использовании через API. Сегодня мы расскажем как начать пользоваться лежащими в его основе генеративными моделями через <strong>YandexGPT API</strong> и сервис <strong>Foundation Models</strong>.</p><p>Итак, для того чтобы начать, нам необходимо:</p><ul><li>Зайти в облачную <a href="https://console.yandex.cloud/link/yandexgpt">консоль управления Yandex Cloud</a>.</li><li>Авторизоваться или, если у вас ещё нет аккаунта, зарегистрироваться.</li><li>Среди списка сервисов выбрать и перейти на страницу <a href="https://console.yandex.cloud/link/yandexgpt">Foundation Models</a>.</li></ul><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/services.webp" alt="Список сервисов" title="Список сервисов"></p><p>Сервис <strong>Foundation Models</strong> объединяет в себе несколько больших генеративных нейросетей.</p><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/overview.webp" alt="Foundation Models" title="Foundation Models"></p><p>На данный момент нам доступны несколько режимов их использования:</p><ul><li>Первый и самый простой - <strong>чат с YandexGPT</strong>. Это по сути аналог всем известного <strong>ChatGPT</strong>.</li></ul><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/chat1.webp" alt="Чат с YandexGPT" title="Чат с YandexGPT"></p><p>В этом режиме в модель прокидывается контекст (история предыдущих запросов). Поэтому работа в таком режиме больше похожа на диалог с ИИ, но не всегда подходит для прикладных бизнес задач.</p><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/chat2.webp" alt="Чат с YandexGPT" title="Чат с YandexGPT"></p><ul><li>Второй режим - это <strong>YandexART</strong>. Генеративная модель для создания изображений из текстового описания.</li></ul><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/art2.webp" alt="YandexART" title="YandexART"></p><p>По умолчанию нейросеть отдаёт изображение размером <strong>1024x1024</strong> пикселя. В формате JPEG.</p><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/art3.jpeg" alt="Пример изображения" title="Пример изображения"></p><ul><li>Ну и третий режим - это <strong>промт-режим</strong>. Он позволяет тонко настраивать поведение модели и по сути является песочницей, с помощью которой можно проверить интеграцию с API до того как выкатывать её в продакшен. Контекст не сохраняется автоматически, но вы можете прокидывать его в запросы.</li></ul><p><img src="https://baranov.guru/assets/posts/yandex-gpt-intro/promt1.webp" alt="Промт-режим" title="Промт-режим"></p><p>В следующих статьях я подробнее рассмотрю <strong>промт-режим</strong>. Расскажу про текущие квоты и лимиты, стоимость, синхронный и асинхронный режимы, эмбеддинги, а также про различия в доступных моделях генерации.</p><p><a href="https://youtu.be/pRHaXBs7RsQ">Посмотреть видео</a></p><p>На этом всё, спасибо за внимание!</p>]]></content:encoded>
            <enclosure url="https://baranov.guru/assets/posts/yandex-gpt-intro/cover.webp" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>