
Алексей Баранов
Подключение счётчика Яндекс Метрики к Next.js приложению
Одним из важнейших аспектов поддержки любого сайта является работа с аналитикой.
Аналитические счётчики для сайтов играют ключевую роль в понимании поведения посетителей и оптимизации веб-ресурсов. Они позволяют владельцам сайтов собирать ценные данные о посетителях, источниках переходов, поведении пользователей на страницах сайта и многом другом.
В этой статье я расскажу о нескольких способах подключения счётчика Яндекс Метрики к Next.js приложению.
Однако, все эти способы очень похожи и для того чтобы лучше их понять и грамотнее их использовать, начнём с написания собственного решения.
Написание собственного решения
Итак, нам нужно:
- создать компонент, который будет загружать счётчик на страницу,
- добавить этот компонент в Root Layout,
- научиться обрабатывать события перехода между страницами,
- а также научиться прокидывать любые другие события в счётчик
Создание компонента
Итак, наш компонент должен:
- Загрузить скрипт Яндекс Метрики с указанными настройками;
- Предоставить методы для работы с 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.
Итак, для этого нам нужно:
- создать компонент, который будет загружать счётчик и передавать ему события,
- подписаться на события перехода между страницами,
- добавить компонент в Root Layout,
- не прокидывать события при локальной отладке страниц
Создание компонента
Сначала установим пакет:
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
Тут всё довольно просто, я решил добавить компонент после закрывающего тега </body>.
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 🙂



