Для любого публичного сайта необходима аналитика, чтобы отслеживать количество посетителей. Особенно это важно для Интернет-магазина: очень важно иметь как можно больше данных о поведении покупателей.
Самое простое, что можно сделать вначале это установить скрипт Yandex Метрики на свой сайт.
В случае использования фреймворка Next.js нужно определиться каким образом интегрировать html-код Яндекс метрики в разметку React.js.
При этом не хотелось бы помещать шаблон скрипта Яндекс метрики в репозиторий кода.
На своём сайте я поместил все скрипты аналитики и счётчиков (в частности счётчик Mail.ru) в один простой html файл.
<!-- Top.Mail.Ru counter --><script type="text/javascript"> var _tmr = window._tmr || (window._tmr = []); _tmr.push({ id: "###", type: "pageView", start: new Date().getTime() }); (function (d, w, id) { if (d.getElementById(id)) return; var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id; ts.src = "https://top-fwz1.mail.ru/js/code.js"; var f = function () { var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s); }; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "tmr-code");</script><noscript ><div> <img src="https://top-fwz1.mail.ru/counter?id=###;js=na" style="position: absolute; left: -9999px" alt="Top.Mail.Ru" /></div></noscript><!-- /Top.Mail.Ru counter --><!-- Yandex.Metrika counter --><script type="text/javascript"> (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("###", "init", { clickmap: true, trackLinks: true, accurateTrackBounce: true, });</script><noscript ><div> <img src="https://mc.yandex.ru/watch/###" style="position: absolute; left: -9999px" alt="" /></div></noscript><!-- /Yandex.Metrika counter --><!--LiveInternet counter--><script> new Image().src = "https://counter.yadro.ru/hit?r" + escape(document.referrer) + (typeof screen == "undefined" ? "" : ";s" + screen.width + "*" + screen.height + "*" + (screen.colorDepth ? screen.colorDepth : screen.pixelDepth)) + ";u" + escape(document.URL) + ";h" + escape(document.title.substring(0, 150)) + ";" + Math.random();</script><!--/LiveInternet--><!-- Top.Mail.Ru logo --><a href="https://top-fwz1.mail.ru/jump?from=###"> <img src="https://top-fwz1.mail.ru/counter?id=###;t=466;l=1" height="31" width="88" alt="Top.Mail.Ru" style="border: 0"/></a><!-- /Top.Mail.Ru logo --><!--LiveInternet logo--><a href="https://www.liveinternet.ru/click" target="_blank" ><img src="https://counter.yadro.ru/logo?52.5" title="LiveInternet: показано число просмотров и посетителей за 24 часа" alt="" style="border: 0" width="88" height="31"/></a><!--/LiveInternet-->
Теперь возникает вопрос, как встроить этот код в приложение Next.js.
Удобнее всего было бы как-то запихнуть всё это в переменную окружения process.env.
Так как этот текст html содержит переносы строк, я не стал использовать стандартный dotenv плагин webpack.
Я поместил в файл конфигурации next.config.js такой код:
/** @type {import('next').NextConfig} */const fs = require("fs");
module.exports = { webpack(config, { buildId, dev, isServer, defaultLoaders, webpack }) { config.plugins.push( new webpack.DefinePlugin({ "process.env.metrika_counters": JSON.stringify( fs.readFileSync("webpack-define-plugin/metrika_counters.html", { enc: "utf-8", }), ), }), ); return config; },};
Webpack Define Plugin просто заменяет указанную в двойных кавычках переменную process.env.metrika_counters по её имени во всём проекте на значение, которое мы указали: читаем html файл с кодом аналитики. Вызов JSON.stringify нужен для того, чтобы завернуть строку в кавычки, а то сам webpack-define-plugin этого не понимает (будет ошибка компиляции).
Я создал папку "webpack-define-plugin" в корне проекта и добавил её в файл .gitignore как "/webpack-define-plugin/**".
Теперь чтобы интегрировать html-код в структуру файла React jsx (tsx для typescript) нужно придумать что-то вроде парсера html кода в разметку React. В случае Next.js для тэгов script, img и a (ссылка) нужно использовать предоставляемые разработчиками Next.js компоненты: Script (next/script), Image (next/image), Link (next/link).
Я нашёл хороший пакет npm:
npm install html-react-parser
Это компонент, который принимает на вход строку с html-кодом и парсит её генерируя компоненты React.
Вот мой компонент для скриптов аналитики:
const options = { trim: true, replace: (in_domNode: DOMNode) => { if (in_domNode.type === "text") { const domNode: Text = in_domNode as Text; return domNode; } else if (in_domNode.type === "style") { return <></>; } else if (in_domNode.type === "script") { const domNode: Element = in_domNode as Element; const { attribs, children } = domNode; const convertedProps = attributesToProps(attribs); const Children: string | JSX.Element | JSX.Element[] | undefined = children && <>{domToReact(children, options)}</>; return ( <Script {...(convertedProps as any)} id={`inline-script-id-${crc32(Children?.props?.children || "")}`} strategy="afterInteractive" > {`${Children?.props?.children || ""}`} </Script> ); } else if (in_domNode.type === "tag" && (in_domNode as Element).name) { const domNode: Element = in_domNode as Element; const { attribs, children } = domNode; const convertedProps = attributesToProps(attribs); const Children: string | JSX.Element | JSX.Element[] | undefined = children && <>{domToReact(children, options)}</>; if (typeof convertedProps.children !== "undefined") { delete convertedProps.children; } if (domNode.name === "noscript") { return domNode; } else if (domNode.name === "a" && convertedProps.href) { return ( <Link {...(convertedProps as any)} target="_blank"> {Children} </Link> ); } else if (domNode.name === "img") { // case "img": const src = convertedProps.src || ""; if (!src) { return null; } const width = parseInt(convertedProps.width || "1") || 1; delete convertedProps.width; const height = parseInt(convertedProps.height || "1") || 1; delete convertedProps.height; const alt = convertedProps.alt || ""; return ( <Image {...(convertedProps as any)} src={src} alt={alt} width={width} height={height} unoptimized loading="lazy" /> ); } else { const styleSX = convertedProps.style || undefined; if (typeof convertedProps.style !== "undefined") { delete (convertedProps as any).style; } return ( <Box component={domNode.name} {...(convertedProps as any)} sx={{ ...styleSX }} > {Children} </Box> ); } } else { return in_domNode; } },};export default memo<Props>(function MetrikaScriptsParser({ htmlString,}: Props) { if (htmlString) { return <>{parse(htmlString, options)}</>; } else { return null; }});
Метод replace объекта options отвечает за создание React компонентов для каждого скрипта или изображения.
Используется специальный компонент Script из фрэймворка Next.js:
import Script from "next/script";
Он принимает тело скрипта аналитики в виде строки внутрь тега (children).
Этому компоненту требуется уникальный атрибут id (идентификатор). Я его делаю уникальным просто вычисляя контрольную сумму crc32 от строки тела скрипта.
Также для изображений счётчиков (я использую счётчики-картинки mail.ru и liveinternet) используется компонент Image из next/image. Он требует обязательно указанных атрибутов width и height (ширина и длина изображения). У него обязательно нужно указать атрибут unoptimized, чтобы next.js не изменял изображение.