@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 +7 -0
- package/analytics.tsx +3 -3
- package/blog.tsx +2 -2
- package/dist/analytics.js +3 -3
- package/dist/blog.js +2 -2
- package/dist/jsonld.d.ts +1 -1
- package/dist/jsonld.js +9 -6
- package/jsonld.tsx +16 -7
- package/package.json +1 -1
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:
|
|
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:
|
|
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
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
17
|
-
|
|
20
|
+
// Параллельно: узлы JSON-LD + элемент аналитики (сам вернёт null, если ID не заданы на платформе).
|
|
21
|
+
const [nodes, analytics] = await Promise.all([seoJsonLd(route, locale), SeoAnalytics({ route, locale })]);
|
|
18
22
|
return (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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