Подключение счётчика Яндекс Метрики к Next.js приложению
13 мин.

Подключение счётчика Яндекс Метрики к Next.js приложению

Одним из важнейших аспектов поддержки любого сайта является работа с аналитикой.

Аналитические счётчики для сайтов играют ключевую роль в понимании поведения посетителей и оптимизации веб-ресурсов. Они позволяют владельцам сайтов собирать ценные данные о посетителях, источниках переходов, поведении пользователей на страницах сайта и многом другом.

В этой статье я расскажу о нескольких способах подключения счётчика Яндекс Метрики к Next.js приложению.

Однако, все эти способы очень похожи и для того чтобы лучше их понять и грамотнее их использовать, начнём с написания собственного решения.

Написание собственного решения

Итак, нам нужно:

Создание компонента

Итак, наш компонент должен:

  • Загрузить скрипт Яндекс Метрики с указанными настройками;
  • Предоставить методы для работы с API Яндекс Метрики;

Загрузка скрипта

Добавим компонент YandexMetrikaInitializer.

Он будет принимать id счётчика и набор параметров счётчика и рендерить Script-компонент, с кодом загрузки счётчика метрики с указанными параметрами.

Это тот самый код, который можно получить в личном кабинете Яндекс Метрики при создании счётчика.

В итоге должно получиться нечто похожее:

"use client";

import Script from "next/script";
import React from "react";

import { YandexMetrikaInitParameters } from "./types";

type Props = {
  id: number;
  initParameters: YandexMetrikaInitParameters;
};

const YandexMetrikaInitializer: React.FC<Props> = ({ id, initParameters }) => {
  /* eslint-disable @next/next/no-img-element */
  return (
    <>
      <Script type="text/javascript" id={`ym_${id}`}>
        {`(function (m, e, t, r, i, k, a) {
  m[i] =
    m[i] ||
    function () {
      (m[i].a = m[i].a || []).push(arguments);
    };
  m[i].l = 1 * new Date();
  for (var j = 0; j < document.scripts.length; j++) {
    if (document.scripts[j].src === r) {
      return;
    }
  }
  (k = e.createElement(t)),
    (a = e.getElementsByTagName(t)[0]),
    (k.async = 1),
    (k.src = r),
    a.parentNode.insertBefore(k, a);
})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

ym(${id}, "init", ${JSON.stringify(initParameters)});`}
      </Script>
      <noscript>
        <div>
          <img
            src={`https://mc.yandex.ru/watch/${id}`}
            style={{ position: "absolute", left: "-9999px;" }}
            alt=""
          />
        </div>
      </noscript>
    </>
  );
};

export default MyYandexMetrikaInitializer;

Набор параметров счётчика

Вот список параметров счётчика, взятый из официальной доки.

export type YandexMetrikaInitParameters = {
  accurateTrackBounce?: boolean | number;
  childIframe?: boolean;
  clickmap?: boolean;
  defer?: boolean;
  ecommerce?: boolean | string | [];
  params?: unknown | [];
  userParams?: unknown;
  trackHash?: boolean;
  trackLinks?: boolean;
  trustedDomains?: string[];
  type?: number;
  webvisor?: boolean;
  triggerEvent?: boolean;
  sendTitle?: boolean;
};

Добавление компонента в Root Layout

Теперь нам надо добавить наш компонент в Root Layout.

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html>
      <head>
        ...
        <YandexMetrikaInitializer
          id={id}
          initParameters={{ webvisor: true, defer: true }}
        />
      </head>
    </html>
  );
}

Обратите внимание, что инициализировать компонент лучше внутри тега head.

Обработка перехода между страницами

При переходе между страницами Next.js не всегда перерендеривает весь Layout.

Это одна из встроенных в фреймворк оптимизаций, она позволяет снизить время перехода между страницами приложения.

Однако нам сейчас это поведение вредит и не позволяет полноценно отслеживать переходы посетителя между страницами.

Как это победить? Надо просто подписаться на события изменения URL. Сделать это можно используя хуки usePathname и useSearchParams встроенного пакета next/navigation, а также воспользовавшись react-хуком useEffect.

Обернём наш компонент для инициализации в контейнер. Назовём его YandexMetrikaContainer и подпишемся на изменения URL.

"use client";

import { usePathname, useSearchParams } from "next/navigation";
import React, { useEffect } from "react";

import { YM_COUNTER_ID } from "./constants";
import YandexMetrikaInitializer from "./YandexMetrikaInitializer";

type Props = {
  enabled: boolean;
};

const YandexMetrikaContainer: React.FC<Props> = ({ enabled }) => {
  const pathname = usePathname();
  const search = useSearchParams();

  useEffect(() => {
    console.log(
      `${pathname}${search.size ? `?${search}` : ""}${window.location.hash}`,
    );
  }, [hit, pathname, search]);

  if (!enabled) return null;

  return (
    <YandexMetrikaInitializer
      id={YM_COUNTER_ID}
      initParameters={{ webvisor: true, defer: true }}
    />
  );
};

export default YandexMetrikaContainer;

Отлично, теперь мы можем видеть в консоли сообщение о том что адрес страницы изменился. Но как передать эту информацию в счётчик?

Для этого нужно воспользоваться одним из встроенных методов Яндекс Метрики под названием hit.

Для удобства, давайте обернём вызов этого метода в хук и назовём его useYandexMetrika.

import { YandexMetrikaHitOptions, YandexMetrikaMethod } from "./types";

declare const ym: (
  id: number,
  method: YandexMetrikaMethod,
  ...params: unknown[]
) => void;

const enabled = !!(process.env.NODE_ENV === "production");

const useYandexMetrika = (id: number) => {
  const hit = (url?: string, options?: YandexMetrikaHitOptions) => {
    if (enabled) {
      ym(id, "hit", url, options);
    } else {
      console.log(`%c[YandexMetrika](hit)`, `color: orange`, url);
    }
  };

  return { hit };
};

export default useYandexMetrika;

Вот так выглядит описание типов YandexMetrikaHitOptions и YandexMetrikaHitParams:

export type YandexMetrikaHitOptions = {
  callback: () => void;
  ctx: unknown;
  params: YandexMetrikaHitParams;
  referer: string;
  title: string;
};

export type YandexMetrikaHitParams = {
  order_price: number;
  currency: string;
};

Теперь используем наш хук при переходе между страницами, должно получиться примерно так:

"use client";

import { usePathname, useSearchParams } from "next/navigation";
import React, { useEffect } from "react";

import { YM_COUNTER_ID } from "./constants";
import useYandexMetrika from "./useYandexMetrika";
import YandexMetrikaInitializer from "./YandexMetrikaInitializer";

type Props = {
  enabled: boolean;
};

const YandexMetrikaContainer: React.FC<Props> = ({ enabled }) => {
  const pathname = usePathname();
  const search = useSearchParams();
  const { hit } = useYandexMetrika(YM_COUNTER_ID);

  useEffect(() => {
    hit(`${pathname}${search.size ? `?${search}` : ""}${window.location.hash}`);
  }, [hit, pathname, search]);

  if (!enabled) return null;

  return (
    <YandexMetrikaInitializer
      id={YM_COUNTER_ID}
      initParameters={{ webvisor: true, defer: true }}
    />
  );
};

export default YandexMetrikaContainer;

Важно! Если вы используете серверный рендеринг, не забудьте обернуть YandexMetrikaContainer в Suspense-тег для того чтобы не сломать его.

import { Suspense } from "react";

...
<Suspense>
  <YandexMetrikaContainer enabled />
</Suspense>;

Отправка событий в счётчик

Отправить любое другое событие в счётчик можно похожим образом.

Просто расширим хук useYandexMetrika новыми методами, например для использования события reachGoal, можно добавить следующий метод:

const reachGoal = (
  target: string,
  params?: unknown,
  callback?: () => void,
  ctx?: unknown,
) => {
  if (enabled) {
    ym(id, "reachGoal", target, params, callback, ctx);
  } else {
    console.log(`%c[YandexMetrika](reachGoal)`, `color: orange`, target);
  }
};

В итоге код нашего хука будет выглядеть следующим образом:

import { YandexMetrikaHitOptions, YandexMetrikaMethod } from "./types";

declare const ym: (
  id: number,
  method: YandexMetrikaMethod,
  ...params: unknown[]
) => void;

const enabled = !!(process.env.NODE_ENV === "production");

const useYandexMetrika = (id: number) => {
  const hit = (url?: string, options?: YandexMetrikaHitOptions) => {
    if (enabled) {
      ym(id, "hit", url, options);
    } else {
      console.log(`%c[YandexMetrika](hit)`, `color: orange`, url);
    }
  };

  const reachGoal = (
    target: string,
    params?: unknown,
    callback?: () => void,
    ctx?: unknown,
  ) => {
    if (enabled) {
      ym(id, "reachGoal", target, params, callback, ctx);
    } else {
      console.log(`%c[YandexMetrika](reachGoal)`, `color: orange`, target);
    }
  };

  return { hit, reachGoal };
};

export default useYandexMetrika;

Использовать другие методы можно аналогичным образом.

export type YandexMetrikaMethod =
  | "init"
  | "hit"
  | "addFileExtension"
  | "extLink"
  | "file"
  | "firstPartyParams"
  | "firstPartyParamsHashed"
  | "getClientID"
  | "notBounce"
  | "params"
  | "reachGoal"
  | "setUserID"
  | "userParams";

На этом всё, по большому счёту, для использования Яндекс Метрики в Next.js приложении больше ничего делать не надо.

Использование пакета react-yandex-metrika

Можно написать своё решение, а можно воспользоваться одним из уже готовых пакетов, например react-yandex-metrika.

Итак, для этого нам нужно:

Создание компонента

Сначала установим пакет:

npm i react-yandex-metrika

Далее нам необходимо создать компонент, назовём его YandexMetrikaContainer.

Так как счётчик должен быть Клиентским компонентом, то в файл с компонентом, перед всеми импортами, необходимо добавить директиву "use client".

"use client";

Далее создаём простой функциональный компонент, использующий YMInitializer из пакета react-yandex-metrika.

Прокидываем в него номер счётчика и другие необходимые нам настройки счётчика.

"use client";

import React from "react";
import { YMInitializer } from "react-yandex-metrika";

const YM_COUNTER_ID = 12345678; //Не настоящий :)

const YandexMetrikaContainer: React.FC = () => {
  return (
    <YMInitializer
      accounts={[YM_COUNTER_ID]}
      options={{
        defer: true,
        webvisor: true,
        clickmap: true,
        trackLinks: true,
        accurateTrackBounce: true,
      }}
      version="2"
    />
  );
};

export default YandexMetrikaContainer;

Но это ещё не всё, помимо загрузки счётчика, нам нужно отправить в него событие попадания на страницу.

import ym from "react-yandex-metrika";

const hit = (url: string) => {
  ym("hit", url);
};

useEffect(() => {
  hit(window.location.pathname + window.location.search);
}, []);

На этом бы можно было остановиться, но при переходе между страницами Next.js не рендерит весь Root Layout, поэтому событие будет вызвано только 1 раз.

Что бы исправить ситуацию, нам нужно подписаться на событие перехода между страницами.

Подписка на события перехода по страницам

Next.js Содержит в себе пакет Router, который позволяет подписаться на нужное нам событие.

Выглядит это следующим образом:

import Router from "next/router";

Router.events.on("routeChangeComplete", callback);

В итоге у нас должно получиться вот так:

import Router from "next/router";
import ym from "react-yandex-metrika";

const hit = (url: string) => {
  ym("hit", url);
};

useEffect(() => {
  hit(window.location.pathname + window.location.search);
  Router.events.on("routeChangeComplete", (url: string) => hit(url));
}, []);

Теперь настало время добавить компонент в Root Layout.

Добавление в Root Layout

Тут всё довольно просто, я решил добавить компонент после закрывающего тега .

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ru" className="scroll-smooth">
      <head>
        ...
      </head>
      <body className={inter.className}>
        ...
      </body>
      <YandexMetrikaContainer/>
    </html>
  );
}

Работа в Dev режиме

Также мне не нужно чтобы счётчик срабатывал когда я запускаю приложение в режиме отладки, поэтому неплохо бы добавить какой-то признак того что мы в режиме отладки

const analyticsEnabled = !!(process.env.NODE_ENV === "production");

В итоге использование компонента будет выглядеть следующим образом:

<YandexMetrikaContainer enabled={analyticsEnabled} />

Итоговый результат

Итоговый код компонента YandexMetrikaContainer

"use client";

import Router from "next/router";
import React, { useCallback, useEffect } from "react";
import ym, { YMInitializer } from "react-yandex-metrika";

type Props = {
  enabled: boolean;
};

const YM_COUNTER_ID = 12345678; //Не настоящий :)

const YandexMetrikaContainer: React.FC<Props> = ({ enabled }) => {
  const hit = useCallback(
    (url: string) => {
      if (enabled) {
        ym("hit", url);
      } else {
        console.log(`%c[YandexMetrika](HIT)`, `color: orange`, url);
      }
    },
    [enabled],
  );

  useEffect(() => {
    hit(window.location.pathname + window.location.search);
    Router.events.on("routeChangeComplete", (url: string) => hit(url));
  }, [hit]);

  if (!enabled) return null;

  return (
    <YMInitializer
      accounts={[YM_COUNTER_ID]}
      options={{
        defer: true,
        webvisor: true,
        clickmap: true,
        trackLinks: true,
        accurateTrackBounce: true,
      }}
      version="2"
    />
  );
};

export default YandexMetrikaContainer;

Итоговый код компонента Root Layout


const analyticsEnabled = !!(process.env.NODE_ENV === "production");

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ru" className="scroll-smooth">
      <head>
        ...
      </head>
      <body className={inter.className}>
        ...
      </body>
      <YandexMetrikaContainer enabled={analyticsEnabled} />
    </html>
  );
}

Альтернативные решения

Вот ещё пара похожих пакетов react-metrika и next-yandex-metrika.

Все они делают примерно одно и то же. Подробно останавливаться на них не вижу смысла.

На этом всё, спасибо за внимание! 🎉

Подписывайтесь на мой Youtube канал и на Telegram 🙂

Поделиться

Поддержать автора

Вам может быть интересно:

Добавляем Google Analytics в Next.js приложение

Добавляем Google Analytics в Next.js приложение

2 мин.

Инструкция по добавлению Google Analytics в Next.js приложение...

Добавляем robots.txt в Next.js приложение

Добавляем robots.txt в Next.js приложение

5 мин.

Инструкция по добавлению robots.txt в Next.js приложение...