@webnumseoagent/next 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -56,6 +56,13 @@ Vercel и `git push`. **Проверь `git diff`** перед коммитом
56
56
  ```
57
57
  Для страниц с сегментами (`/[slug]`) передавай реальный `route` (напр. `/projects/${slug}`).
58
58
 
59
+ **Аналитика (GA4 / Google Tag Manager / Яндекс.Метрика) — с версии 0.5.0 включается автоматически:**
60
+ `<SeoJsonLd/>` заодно рендерит счётчики, ID которых заданы на платформе (вкладка «Аналитика»).
61
+ Отдельно ничего монтировать не нужно — обновил пакет, передеплоил, и счётчики включаются/выключаются
62
+ прямо с платформы. Скрипты идемпотентны (несколько `<SeoJsonLd/>` на странице безопасны).
63
+ Если Schema не используешь и `<SeoJsonLd/>` не смонтирован — можно смонтировать только аналитику:
64
+ `import { SeoAnalytics } from "@/seoagent/analytics"` → `<SeoAnalytics />` внутри `<body>`.
65
+
59
66
  4. Создай карту сайта и robots (если их ещё нет):
60
67
  ```ts
61
68
  // app/sitemap.ts
package/analytics.tsx CHANGED
@@ -27,7 +27,7 @@ export async function SeoAnalytics({ route, locale }: { route?: string; locale?:
27
27
  <script
28
28
  // eslint-disable-next-line react/no-danger
29
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}');`,
30
+ __html: `(function(w,d,s,l,i){var g=w.__saGtm=w.__saGtm||{};if(g[i])return;g[i]=1;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
31
  }}
32
32
  />
33
33
  <noscript>
@@ -47,7 +47,7 @@ export async function SeoAnalytics({ route, locale }: { route?: string; locale?:
47
47
  <script
48
48
  // eslint-disable-next-line react/no-danger
49
49
  dangerouslySetInnerHTML={{
50
- __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
50
+ __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}var _g=window.__saGa4=window.__saGa4||{};if(!_g['${ga4}']){_g['${ga4}']=1;gtag('js',new Date());gtag('config','${ga4}');}`,
51
51
  }}
52
52
  />
53
53
  </>
@@ -58,7 +58,7 @@ export async function SeoAnalytics({ route, locale }: { route?: string; locale?:
58
58
  <script
59
59
  // eslint-disable-next-line react/no-danger
60
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});`,
61
+ __html: `var _ym=window.__saYm=window.__saYm||{};if(!_ym[${ym}]){_ym[${ym}]=1;(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
62
  }}
63
63
  />
64
64
  <noscript>
package/blog.tsx CHANGED
@@ -260,7 +260,7 @@ function themeStyle(theme?: SeoBlogTheme): Record<string, string> {
260
260
 
261
261
  // Сетка/карточки списка — через класс + медиазапросы (инлайн-стиль их не умеет): 1→2→3→4 колонки.
262
262
  // Цвета/рамки/скругления берутся из --sa-* (авто-тема сайта). Наследует шрифт/цвет текста от layout.
263
- const GRID_CSS = `.sa-blog-wrap{max-width:1120px;margin:0 auto;padding:2rem 1.25rem}
263
+ const GRID_CSS = `.sa-blog-wrap{max-width:1120px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.25rem 3rem}
264
264
  .sa-blog-h1{font-size:2rem;line-height:1.2;margin:0 0 1.5rem}
265
265
  .sa-blog-grid{display:grid;gap:1.5rem;grid-template-columns:1fr}
266
266
  @media(min-width:560px){.sa-blog-grid{grid-template-columns:repeat(2,1fr)}}
@@ -329,7 +329,7 @@ export async function SeoBlogIndex(props: { locale?: string; basePath?: string;
329
329
  // Одна статья. Принимает пост (из seoBlogPost) — так страница может сделать notFound()/метаданные.
330
330
  export function SeoBlogArticle({ post, locale }: { post: SeoBlogPostFull; locale?: string }) {
331
331
  return (
332
- <article style={{ maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7, ...themeStyle(post.theme) } as CSSProperties}>
332
+ <article style={{ maxWidth: 760, margin: "0 auto", padding: "var(--sa-blog-top, clamp(5rem, 8vw, 7rem)) 1.5rem 3rem", lineHeight: 1.7, ...themeStyle(post.theme) } as CSSProperties}>
333
333
  <style dangerouslySetInnerHTML={{ __html: BODY_CSS }} />
334
334
  {post.cover ? <img src={post.cover} alt={post.title} style={{ width: "100%", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" }} /> : null}
335
335
  <h1 style={{ fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }}>{post.title}</h1>
package/dist/analytics.js CHANGED
@@ -25,14 +25,14 @@ async function SeoAnalytics({ route, locale } = {}) {
25
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
26
  // eslint-disable-next-line react/no-danger
27
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}');`,
28
+ __html: `(function(w,d,s,l,i){var g=w.__saGtm=w.__saGtm||{};if(g[i])return;g[i]=1;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
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
30
  // eslint-disable-next-line react/no-danger
31
31
  dangerouslySetInnerHTML: {
32
- __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
32
+ __html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}var _g=window.__saGa4=window.__saGa4||{};if(!_g['${ga4}']){_g['${ga4}']=1;gtag('js',new Date());gtag('config','${ga4}');}`,
33
33
  } })] })), ym && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", {
34
34
  // eslint-disable-next-line react/no-danger
35
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});`,
36
+ __html: `var _ym=window.__saYm=window.__saYm||{};if(!_ym[${ym}]){_ym[${ym}]=1;(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
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
38
  }
package/dist/blog.js CHANGED
@@ -266,7 +266,7 @@ function themeStyle(theme) {
266
266
  }
267
267
  // Сетка/карточки списка — через класс + медиазапросы (инлайн-стиль их не умеет): 1→2→3→4 колонки.
268
268
  // Цвета/рамки/скругления берутся из --sa-* (авто-тема сайта). Наследует шрифт/цвет текста от layout.
269
- const GRID_CSS = `.sa-blog-wrap{max-width:1120px;margin:0 auto;padding:2rem 1.25rem}
269
+ const GRID_CSS = `.sa-blog-wrap{max-width:1120px;margin:0 auto;padding:var(--sa-blog-top,clamp(5rem,8vw,7rem)) 1.25rem 3rem}
270
270
  .sa-blog-h1{font-size:2rem;line-height:1.2;margin:0 0 1.5rem}
271
271
  .sa-blog-grid{display:grid;gap:1.5rem;grid-template-columns:1fr}
272
272
  @media(min-width:560px){.sa-blog-grid{grid-template-columns:repeat(2,1fr)}}
@@ -304,7 +304,7 @@ async function SeoBlogIndex(props) {
304
304
  }
305
305
  // Одна статья. Принимает пост (из seoBlogPost) — так страница может сделать notFound()/метаданные.
306
306
  function SeoBlogArticle({ post, locale }) {
307
- return ((0, jsx_runtime_1.jsxs)("article", { style: { maxWidth: 760, margin: "0 auto", padding: "1.5rem", lineHeight: 1.7, ...themeStyle(post.theme) }, children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: BODY_CSS } }), post.cover ? (0, jsx_runtime_1.jsx)("img", { src: post.cover, alt: post.title, style: { width: "100%", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" } }) : null, (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }, children: post.title }), post.date ? (0, jsx_runtime_1.jsx)("p", { style: { color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }, children: fmtDate(post.date, locale) }) : null, (0, jsx_runtime_1.jsx)("div", { className: "seoagent-blog-body", dangerouslySetInnerHTML: { __html: post.bodyHtml } })] }));
307
+ return ((0, jsx_runtime_1.jsxs)("article", { style: { maxWidth: 760, margin: "0 auto", padding: "var(--sa-blog-top, clamp(5rem, 8vw, 7rem)) 1.5rem 3rem", lineHeight: 1.7, ...themeStyle(post.theme) }, children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: BODY_CSS } }), post.cover ? (0, jsx_runtime_1.jsx)("img", { src: post.cover, alt: post.title, style: { width: "100%", borderRadius: "var(--sa-radius, 12px)", marginBottom: "1.5rem" } }) : null, (0, jsx_runtime_1.jsx)("h1", { style: { fontSize: "2.2rem", lineHeight: 1.2, margin: "0 0 1rem" }, children: post.title }), post.date ? (0, jsx_runtime_1.jsx)("p", { style: { color: "var(--sa-muted, #94a3b8)", margin: "0 0 1.5rem" }, children: fmtDate(post.date, locale) }) : null, (0, jsx_runtime_1.jsx)("div", { className: "seoagent-blog-body", dangerouslySetInnerHTML: { __html: post.bodyHtml } })] }));
308
308
  }
309
309
  function fmtDate(d, locale) {
310
310
  try {
package/dist/jsonld.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare function SeoJsonLd({ route, locale }: {
2
2
  route: string;
3
3
  locale?: string;
4
- }): Promise<import("react").JSX.Element | null>;
4
+ }): Promise<import("react").JSX.Element>;
package/dist/jsonld.js CHANGED
@@ -5,7 +5,11 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  // Серверный компонент для инъекции JSON-LD. XSS-безопасно: экранируем '<' → <
6
6
  // и не даём закрыть тег </script>. Смонтируй в layout и/или на страницах:
7
7
  // <SeoJsonLd route="/" locale={locale} />
8
+ // Заодно рендерит счётчики аналитики (GA4/GTM/Метрика) через <SeoAnalytics/> — чтобы их можно было
9
+ // включать С ПЛАТФОРМЫ без правки layout сайта (аналитика монтируется вместе со Schema, которая уже стоит).
10
+ // Скрипты аналитики идемпотентны, поэтому несколько <SeoJsonLd/> на странице безопасны.
8
11
  const client_1 = require("./client");
12
+ const analytics_1 = require("./analytics");
9
13
  function safeJson(nodes) {
10
14
  // Один граф Schema.org; экранируем опасные последовательности.
11
15
  const graph = nodes.length === 1 ? nodes[0] : { "@context": "https://schema.org", "@graph": nodes };
@@ -15,10 +19,9 @@ function safeJson(nodes) {
15
19
  .replace(/&/g, "\\u0026");
16
20
  }
17
21
  async function SeoJsonLd({ route, locale }) {
18
- const nodes = await (0, client_1.seoJsonLd)(route, locale);
19
- if (!nodes.length)
20
- return null;
21
- return ((0, jsx_runtime_1.jsx)("script", { type: "application/ld+json",
22
- // eslint-disable-next-line react/no-danger
23
- dangerouslySetInnerHTML: { __html: safeJson(nodes) } }));
22
+ // Параллельно: узлы JSON-LD + элемент аналитики (сам вернёт null, если ID не заданы на платформе).
23
+ const [nodes, analytics] = await Promise.all([(0, client_1.seoJsonLd)(route, locale), (0, analytics_1.SeoAnalytics)({ route, locale })]);
24
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [nodes.length > 0 && ((0, jsx_runtime_1.jsx)("script", { type: "application/ld+json",
25
+ // eslint-disable-next-line react/no-danger
26
+ dangerouslySetInnerHTML: { __html: safeJson(nodes) } })), analytics] }));
24
27
  }
package/jsonld.tsx CHANGED
@@ -1,7 +1,11 @@
1
1
  // Серверный компонент для инъекции JSON-LD. XSS-безопасно: экранируем '<' → <
2
2
  // и не даём закрыть тег </script>. Смонтируй в layout и/или на страницах:
3
3
  // <SeoJsonLd route="/" locale={locale} />
4
+ // Заодно рендерит счётчики аналитики (GA4/GTM/Метрика) через <SeoAnalytics/> — чтобы их можно было
5
+ // включать С ПЛАТФОРМЫ без правки layout сайта (аналитика монтируется вместе со Schema, которая уже стоит).
6
+ // Скрипты аналитики идемпотентны, поэтому несколько <SeoJsonLd/> на странице безопасны.
4
7
  import { seoJsonLd } from "./client";
8
+ import { SeoAnalytics } from "./analytics";
5
9
 
6
10
  function safeJson(nodes: unknown[]): string {
7
11
  // Один граф Schema.org; экранируем опасные последовательности.
@@ -13,13 +17,18 @@ function safeJson(nodes: unknown[]): string {
13
17
  }
14
18
 
15
19
  export async function SeoJsonLd({ route, locale }: { route: string; locale?: string }) {
16
- const nodes = await seoJsonLd(route, locale);
17
- if (!nodes.length) return null;
20
+ // Параллельно: узлы JSON-LD + элемент аналитики (сам вернёт null, если ID не заданы на платформе).
21
+ const [nodes, analytics] = await Promise.all([seoJsonLd(route, locale), SeoAnalytics({ route, locale })]);
18
22
  return (
19
- <script
20
- type="application/ld+json"
21
- // eslint-disable-next-line react/no-danger
22
- dangerouslySetInnerHTML={{ __html: safeJson(nodes) }}
23
- />
23
+ <>
24
+ {nodes.length > 0 && (
25
+ <script
26
+ type="application/ld+json"
27
+ // eslint-disable-next-line react/no-danger
28
+ dangerouslySetInnerHTML={{ __html: safeJson(nodes) }}
29
+ />
30
+ )}
31
+ {analytics}
32
+ </>
24
33
  );
25
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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",