@webnumseoagent/next 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/analytics.tsx ADDED
@@ -0,0 +1,73 @@
1
+ // Серверный компонент: вставляет счётчики аналитики (GA4 / Google Tag Manager / Яндекс.Метрика).
2
+ // Скрипты рендерятся в SSR-HTML и выполняются браузером при загрузке. Смонтируй ОДИН раз в layout:
3
+ // import { SeoAnalytics } from "@webnumseoagent/next/analytics";
4
+ // <body>… <SeoAnalytics /> …</body>
5
+ // Fail-safe: платформа недоступна → seoAnalytics() вернёт {} → ничего не рендерим.
6
+ import { seoAnalytics } from "./client";
7
+
8
+ // ID валидируются на платформе (saveAnalytics), но санитайзим и здесь (defense-in-depth):
9
+ // значения попадают в inline-скрипт, поэтому пропускаем только ожидаемый набор символов.
10
+ // typeof-гард: /config — недоверенный источник, нестроковое значение НЕ должно ронять рендер.
11
+ const ga4Id = (s?: string) => (typeof s === "string" && /^G-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
12
+ const gtmId = (s?: string) => (typeof s === "string" && /^GTM-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
13
+ // Метрика вставляется как ЧИСЛОВОЙ литерал — срезаем ведущие нули (иначе sloppy-mode парсит как octal).
14
+ const ymId = (s?: string) => (typeof s === "string" && /^\d+$/.test(s.trim()) ? s.trim().replace(/^0+(?=\d)/, "") : "");
15
+
16
+ export async function SeoAnalytics({ route, locale }: { route?: string; locale?: string } = {}) {
17
+ const a = await seoAnalytics(route ?? "/", locale);
18
+ const ga4 = ga4Id(a.ga4);
19
+ const gtm = gtmId(a.gtm);
20
+ const ym = ymId(a.metrica);
21
+ if (!ga4 && !gtm && !ym) return null;
22
+
23
+ return (
24
+ <>
25
+ {gtm && (
26
+ <>
27
+ <script
28
+ // eslint-disable-next-line react/no-danger
29
+ dangerouslySetInnerHTML={{
30
+ __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtm}');`,
31
+ }}
32
+ />
33
+ <noscript>
34
+ <iframe
35
+ src={`https://www.googletagmanager.com/ns.html?id=${gtm}`}
36
+ height="0"
37
+ width="0"
38
+ style={{ display: "none", visibility: "hidden" }}
39
+ />
40
+ </noscript>
41
+ </>
42
+ )}
43
+
44
+ {ga4 && (
45
+ <>
46
+ <script async src={`https://www.googletagmanager.com/gtag/js?id=${ga4}`} />
47
+ <script
48
+ // eslint-disable-next-line react/no-danger
49
+ dangerouslySetInnerHTML={{
50
+ __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
51
+ }}
52
+ />
53
+ </>
54
+ )}
55
+
56
+ {ym && (
57
+ <>
58
+ <script
59
+ // eslint-disable-next-line react/no-danger
60
+ dangerouslySetInnerHTML={{
61
+ __html: `(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(${ym},"init",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true});`,
62
+ }}
63
+ />
64
+ <noscript>
65
+ <div>
66
+ <img src={`https://mc.yandex.ru/watch/${ym}`} style={{ position: "absolute", left: "-9999px" }} alt="" />
67
+ </div>
68
+ </noscript>
69
+ </>
70
+ )}
71
+ </>
72
+ );
73
+ }
package/client.ts CHANGED
@@ -21,10 +21,12 @@ interface SeoFields {
21
21
  description?: string;
22
22
  canonical?: string;
23
23
  robots?: { index?: boolean; follow?: boolean };
24
- og?: { title?: string; description?: string; image?: string };
25
- twitter?: { card?: string; image?: string };
24
+ og?: { title?: string; description?: string; image?: string; siteName?: string; locale?: string };
25
+ twitter?: { card?: string; image?: string; site?: string };
26
+ facebook?: { appId?: string; admins?: string }; // fb:app_id / fb:admins (admins — через запятую)
26
27
  jsonLd?: unknown[];
27
28
  verification?: Record<string, string>; // проверочные мета-теги: metaName → content
29
+ analytics?: { ga4?: string; gtm?: string; metrica?: string }; // счётчики (см. analytics.tsx)
28
30
  }
29
31
 
30
32
  async function fetchJson<T>(path: string, revalidate = 300): Promise<T | null> {
@@ -67,6 +69,8 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
67
69
  ...(f.og.title ? { title: f.og.title } : {}),
68
70
  ...(f.og.description ? { description: f.og.description } : {}),
69
71
  ...(f.og.image ? { images: [f.og.image] } : {}),
72
+ ...(f.og.siteName ? { siteName: f.og.siteName } : {}),
73
+ ...(f.og.locale ? { locale: f.og.locale } : {}),
70
74
  };
71
75
  }
72
76
  if (f.twitter) {
@@ -74,8 +78,19 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
74
78
  ...(m.twitter ?? {}),
75
79
  ...(f.twitter.card ? { card: f.twitter.card as "summary" | "summary_large_image" } : {}),
76
80
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
81
+ ...(f.twitter.site ? { site: f.twitter.site } : {}),
77
82
  };
78
83
  }
84
+ // fb:app_id / fb:admins — только через нативный metadata.facebook (property=, не name=).
85
+ // Тип Next — union (appId XOR admins), поэтому собираем объект и приводим к типу.
86
+ if (f.facebook && (f.facebook.appId || f.facebook.admins)) {
87
+ const admins = (f.facebook.admins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
88
+ m.facebook = {
89
+ ...(m.facebook ?? {}), // как и остальные блоки — накладываемся поверх дефолта сайта, не затирая его
90
+ ...(f.facebook.appId ? { appId: f.facebook.appId } : {}),
91
+ ...(admins.length ? { admins: admins.length === 1 ? admins[0] : admins } : {}),
92
+ } as Metadata["facebook"];
93
+ }
79
94
  // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
80
95
  // Мержим, не затирая существующий other сайта.
81
96
  if (f.verification && typeof f.verification === "object") {
@@ -168,3 +183,11 @@ export async function seoJsonLd(route: string, locale?: string): Promise<unknown
168
183
  );
169
184
  return data?.fields?.jsonLd ?? [];
170
185
  }
186
+
187
+ // Счётчики аналитики (общесайтовые). Читаются в analytics.tsx (<SeoAnalytics/>).
188
+ export async function seoAnalytics(route = "/", locale?: string): Promise<{ ga4?: string; gtm?: string; metrica?: string }> {
189
+ const data = await fetchJson<{ fields: SeoFields }>(
190
+ `/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`,
191
+ );
192
+ return data?.fields?.analytics ?? {};
193
+ }
@@ -0,0 +1,4 @@
1
+ export declare function SeoAnalytics({ route, locale }?: {
2
+ route?: string;
3
+ locale?: string;
4
+ }): Promise<import("react").JSX.Element | null>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SeoAnalytics = SeoAnalytics;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ // Серверный компонент: вставляет счётчики аналитики (GA4 / Google Tag Manager / Яндекс.Метрика).
6
+ // Скрипты рендерятся в SSR-HTML и выполняются браузером при загрузке. Смонтируй ОДИН раз в layout:
7
+ // import { SeoAnalytics } from "@webnumseoagent/next/analytics";
8
+ // <body>… <SeoAnalytics /> …</body>
9
+ // Fail-safe: платформа недоступна → seoAnalytics() вернёт {} → ничего не рендерим.
10
+ const client_1 = require("./client");
11
+ // ID валидируются на платформе (saveAnalytics), но санитайзим и здесь (defense-in-depth):
12
+ // значения попадают в inline-скрипт, поэтому пропускаем только ожидаемый набор символов.
13
+ // typeof-гард: /config — недоверенный источник, нестроковое значение НЕ должно ронять рендер.
14
+ const ga4Id = (s) => (typeof s === "string" && /^G-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
15
+ const gtmId = (s) => (typeof s === "string" && /^GTM-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
16
+ // Метрика вставляется как ЧИСЛОВОЙ литерал — срезаем ведущие нули (иначе sloppy-mode парсит как octal).
17
+ const ymId = (s) => (typeof s === "string" && /^\d+$/.test(s.trim()) ? s.trim().replace(/^0+(?=\d)/, "") : "");
18
+ async function SeoAnalytics({ route, locale } = {}) {
19
+ const a = await (0, client_1.seoAnalytics)(route ?? "/", locale);
20
+ const ga4 = ga4Id(a.ga4);
21
+ const gtm = gtmId(a.gtm);
22
+ const ym = ymId(a.metrica);
23
+ if (!ga4 && !gtm && !ym)
24
+ return null;
25
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [gtm && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", {
26
+ // eslint-disable-next-line react/no-danger
27
+ dangerouslySetInnerHTML: {
28
+ __html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtm}');`,
29
+ } }), (0, jsx_runtime_1.jsx)("noscript", { children: (0, jsx_runtime_1.jsx)("iframe", { src: `https://www.googletagmanager.com/ns.html?id=${gtm}`, height: "0", width: "0", style: { display: "none", visibility: "hidden" } }) })] })), ga4 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${ga4}` }), (0, jsx_runtime_1.jsx)("script", {
30
+ // eslint-disable-next-line react/no-danger
31
+ dangerouslySetInnerHTML: {
32
+ __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
33
+ } })] })), ym && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", {
34
+ // eslint-disable-next-line react/no-danger
35
+ dangerouslySetInnerHTML: {
36
+ __html: `(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(${ym},"init",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true});`,
37
+ } }), (0, jsx_runtime_1.jsx)("noscript", { children: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("img", { src: `https://mc.yandex.ru/watch/${ym}`, style: { position: "absolute", left: "-9999px" }, alt: "" }) }) })] }))] }));
38
+ }
package/dist/client.d.ts CHANGED
@@ -11,3 +11,8 @@ export declare function seoSitemap(): Promise<MetadataRoute.Sitemap>;
11
11
  export declare function seoRobots(): Promise<MetadataRoute.Robots>;
12
12
  export declare function seoRobotsTxt(): Promise<string>;
13
13
  export declare function seoJsonLd(route: string, locale?: string): Promise<unknown[]>;
14
+ export declare function seoAnalytics(route?: string, locale?: string): Promise<{
15
+ ga4?: string;
16
+ gtm?: string;
17
+ metrica?: string;
18
+ }>;
package/dist/client.js CHANGED
@@ -18,6 +18,7 @@ exports.seoSitemap = seoSitemap;
18
18
  exports.seoRobots = seoRobots;
19
19
  exports.seoRobotsTxt = seoRobotsTxt;
20
20
  exports.seoJsonLd = seoJsonLd;
21
+ exports.seoAnalytics = seoAnalytics;
21
22
  const API = process.env.SEOAGENT_API_BASE ?? "";
22
23
  const SITE = process.env.SEOAGENT_SITE_ID ?? "";
23
24
  const TOKEN = process.env.SEOAGENT_TOKEN ?? "";
@@ -66,6 +67,8 @@ function mergeIntoMetadata(fallback, f) {
66
67
  ...(f.og.title ? { title: f.og.title } : {}),
67
68
  ...(f.og.description ? { description: f.og.description } : {}),
68
69
  ...(f.og.image ? { images: [f.og.image] } : {}),
70
+ ...(f.og.siteName ? { siteName: f.og.siteName } : {}),
71
+ ...(f.og.locale ? { locale: f.og.locale } : {}),
69
72
  };
70
73
  }
71
74
  if (f.twitter) {
@@ -73,6 +76,17 @@ function mergeIntoMetadata(fallback, f) {
73
76
  ...(m.twitter ?? {}),
74
77
  ...(f.twitter.card ? { card: f.twitter.card } : {}),
75
78
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
79
+ ...(f.twitter.site ? { site: f.twitter.site } : {}),
80
+ };
81
+ }
82
+ // fb:app_id / fb:admins — только через нативный metadata.facebook (property=, не name=).
83
+ // Тип Next — union (appId XOR admins), поэтому собираем объект и приводим к типу.
84
+ if (f.facebook && (f.facebook.appId || f.facebook.admins)) {
85
+ const admins = (f.facebook.admins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
86
+ m.facebook = {
87
+ ...(m.facebook ?? {}), // как и остальные блоки — накладываемся поверх дефолта сайта, не затирая его
88
+ ...(f.facebook.appId ? { appId: f.facebook.appId } : {}),
89
+ ...(admins.length ? { admins: admins.length === 1 ? admins[0] : admins } : {}),
76
90
  };
77
91
  }
78
92
  // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
@@ -160,3 +174,8 @@ async function seoJsonLd(route, locale) {
160
174
  const data = await fetchJson(`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`);
161
175
  return data?.fields?.jsonLd ?? [];
162
176
  }
177
+ // Счётчики аналитики (общесайтовые). Читаются в analytics.tsx (<SeoAnalytics/>).
178
+ async function seoAnalytics(route = "/", locale) {
179
+ const data = await fetchJson(`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`);
180
+ return data?.fields?.analytics ?? {};
181
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Runtime SEO adapter + installer for Next.js App Router sites (SEO Agent platform)",
5
5
  "main": "dist/client.js",
6
6
  "types": "dist/client.d.ts",
@@ -19,12 +19,17 @@
19
19
  "./redirects": {
20
20
  "types": "./dist/redirects.d.ts",
21
21
  "default": "./dist/redirects.js"
22
+ },
23
+ "./analytics": {
24
+ "types": "./dist/analytics.d.ts",
25
+ "default": "./dist/analytics.js"
22
26
  }
23
27
  },
24
28
  "files": [
25
29
  "dist",
26
30
  "client.ts",
27
31
  "jsonld.tsx",
32
+ "analytics.tsx",
28
33
  "sitemap-xsl.ts",
29
34
  "sitemap-build.ts",
30
35
  "redirects.ts",