@webnumseoagent/next 0.1.12 → 0.1.15

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/bin/cli.mjs CHANGED
@@ -262,6 +262,27 @@ async function init() {
262
262
  }
263
263
  }
264
264
 
265
+ // 4.6 Мониторинг 404: роут-приёмник на сайте (форвардит хит в платформу с СЕРВЕРНЫМ
266
+ // токеном) + not-found-репортер (клиентский маячок с путём 404). Репортер отдаём сырым
267
+ // исходником (client-компонент из dist пакета не работает — tsc ставит "use strict" первой).
268
+ {
269
+ const reqType = ext === "ts" ? ": Request" : "";
270
+ const fwd = `export async function POST(req${reqType}) {\n const origin = req.headers.get("origin"), host = req.headers.get("host");\n if (origin && host) { try { if (new URL(origin).host !== host) return new Response(null, { status: 204 }); } catch { return new Response(null, { status: 204 }); } }\n const API = process.env.SEOAGENT_API_BASE, SITE = process.env.SEOAGENT_SITE_ID, TOKEN = process.env.SEOAGENT_TOKEN;\n if (!API || !SITE || !TOKEN) return new Response(null, { status: 204 });\n let path = "";\n try { const b = await req.json(); if (typeof b?.path === "string") path = b.path; } catch {}\n if (!path) return new Response(null, { status: 204 });\n try { await fetch(\`\${API}/api/seo/\${SITE}/not-found\`, { method: "POST", headers: { "content-type": "application/json", "x-seo-agent-key": TOKEN }, body: JSON.stringify({ path }), signal: AbortSignal.timeout(5000) }); } catch {}\n return new Response(null, { status: 204 });\n}\n`;
271
+ await ensureFile(path.join(root, appDir, `api/seoagent-404/route.${ext}`), fwd);
272
+
273
+ const reporter = `"use client";\n\nimport Link from "next/link";\nimport { usePathname } from "next/navigation";\nimport { useEffect, useRef } from "react";\n\n// Страница 404 + репортер для «Мониторинг 404» (SEO Agent).\nexport default function NotFound() {\n const pathname = usePathname();\n const lastSent = useRef(null);\n useEffect(() => {\n const p = pathname || window.location.pathname;\n if (lastSent.current === p) return;\n lastSent.current = p;\n try {\n const body = JSON.stringify({ path: p });\n if (navigator.sendBeacon) navigator.sendBeacon("/api/seoagent-404", new Blob([body], { type: "application/json" }));\n else fetch("/api/seoagent-404", { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).catch(() => {});\n } catch {}\n }, [pathname]);\n return (\n <main style={{ minHeight: "60vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "1rem", padding: "2rem", textAlign: "center" }}>\n <h1 style={{ fontSize: "3rem", margin: 0 }}>404</h1>\n <p style={{ color: "#64748b", margin: 0 }}>Страница не найдена.</p>\n <Link href="/" style={{ color: "#2563eb" }}>На главную</Link>\n </main>\n );\n}\n`;
274
+ const nfExt = ext === "ts" ? "tsx" : "js";
275
+ const hasNotFound = await anyExists([
276
+ `${appDir}/not-found.js`, `${appDir}/not-found.jsx`, `${appDir}/not-found.ts`, `${appDir}/not-found.tsx`,
277
+ ]);
278
+ if (hasNotFound) {
279
+ log(`\n \x1b[1mnot-found уже есть\x1b[0m — добавь репортер 404 (Мониторинг 404) в свой not-found:`);
280
+ log(` \x1b[36m"use client"; ... useEffect(() => { navigator.sendBeacon?.("/api/seoagent-404", new Blob([JSON.stringify({ path: location.pathname })], { type: "application/json" })); }, []);\x1b[0m`);
281
+ } else {
282
+ await ensureFile(path.join(root, appDir, `not-found.${nfExt}`), reporter);
283
+ }
284
+ }
285
+
265
286
  // 5. .env.local
266
287
  const envPath = path.join(root, ".env.local");
267
288
  const cur = (await exists(envPath)) ? await fs.readFile(envPath, "utf8") : "";
@@ -298,7 +319,7 @@ async function init() {
298
319
  log("\n Осталось вручную (замени `return { … }` на):");
299
320
  for (const m of manual) {
300
321
  log(` • ${path.relative(root, m.p)}`);
301
- log(` \x1b[36mimport { seoMeta } from "${clientFor(path.relative(path.join(root, appDir), m.p))}";\n return seoMeta({ route: ${m.expr}, locale, fallback: { /* прежний объект */ } });\x1b[0m`);
322
+ log(` \x1b[36mimport { seoMeta } from "${clientFor(path.relative(path.join(root, appDir), m.p))}";\n return seoMeta({ route: ${paramsExpr(m.expr)}, locale, fallback: { /* прежний объект */ } });\x1b[0m`);
302
323
  }
303
324
  }
304
325
 
@@ -321,7 +342,7 @@ async function init() {
321
342
  const rel = path.relative(path.join(root, appDir), p);
322
343
  const { expr } = routeFromFile(rel);
323
344
  log(`\n • \x1b[1m${path.relative(root, p)}\x1b[0m`);
324
- log(` \x1b[36mreturn seoMeta({ route: ${expr}, locale, fallback: { /* твой прежний объект меты */ } });\x1b[0m`);
345
+ log(` \x1b[36mreturn seoMeta({ route: ${paramsExpr(expr)}, locale, fallback: { /* твой прежний объект меты */ } });\x1b[0m`);
325
346
  }
326
347
  }
327
348
 
@@ -361,14 +382,17 @@ function routeFromFile(relFromApp) {
361
382
  if (!segs.length) return { expr: '"/"', dynamic: false };
362
383
  let dynamic = false;
363
384
  const parts = segs.map((seg) => {
364
- const m = seg.match(/^\[(?:\.\.\.)?(.+)\]$/);
365
- if (m) { dynamic = true; return "${" + m[1] + "}"; }
385
+ const m = seg.match(/^\[+(?:\.\.\.)?(.+?)\]+$/); // [slug], [...slug], [[...slug]]
386
+ if (m) { dynamic = true; return "${" + m[1].replace(/[.\[\]]/g, "") + "}"; }
366
387
  return seg;
367
388
  });
368
389
  const pathStr = "/" + parts.join("/");
369
390
  return { expr: dynamic ? "`" + pathStr + "`" : `"${pathStr}"`, dynamic };
370
391
  }
371
392
 
393
+ // Ручной сниппет вставляется в generateMetadata({ params }) — сегменты идут через params.
394
+ const paramsExpr = (expr) => expr.replace(/\$\{(\w+)\}/g, "${params.$1}");
395
+
372
396
  async function findMetadataPages(dir, acc = []) {
373
397
  for (const e of await fs.readdir(dir, { withFileTypes: true })) {
374
398
  const p = path.join(dir, e.name);
package/bin/wrap.mjs CHANGED
@@ -104,27 +104,45 @@ export async function wrapFile(code, routeExpr, importSpec) {
104
104
  // locale опционально (мультиязычные — есть, одноязычные — нет).
105
105
  const hasLocale = /\blocale\b/.test(code);
106
106
 
107
- // База доступа к params: `generateMetadata({ params })` → "params";
108
- // `generateMetadata(props)` "props.params".
109
- let paramsBase = "params";
110
- const p0 = fn.params && fn.params[0];
111
- if (p0) {
112
- if (p0.type === "ObjectPattern") {
113
- const pp = p0.properties.find((x) => x.key && x.key.name === "params");
114
- if (pp && pp.value && pp.value.name) paramsBase = pp.value.name;
107
+ // Строим корректные обращения к динамическим сегментам. В generateMetadata сегмент
108
+ // ВСЕГДА доступен через объект params (Next всегда передаёт { params }); голое имя
109
+ // корректно ТОЛЬКО если сегмент явно деструктурирован в сигнатуре. Опираемся на САМУ
110
+ // сигнатуру не на скан всего файла — там `id` мог встретиться где угодно) и, если
111
+ // нужно, нормализуем сигнатуру, чтобы params/сегменты гарантированно были в области видимости.
112
+ const segNames = [...routeExpr.matchAll(/\$\{(\w+)\}/g)].map((m) => m[1]);
113
+ let effRoute = routeExpr;
114
+ if (segNames.length) {
115
+ const mkPat = (src) => babelParser.parseExpression(src, { plugins }).params[0]; // паттерн-параметр стрелки
116
+ const destructured = new Set(); // сегменты, связанные как отдельные переменные в сигнатуре
117
+ let base = "params";
118
+ const p0 = fn.params && fn.params[0];
119
+ if (!p0) {
120
+ fn.params = fn.params || [];
121
+ fn.params[0] = mkPat("({ params }) => 0"); // нет параметра → добавляем { params }
115
122
  } else if (p0.type === "Identifier") {
116
- paramsBase = p0.name + ".params";
123
+ base = p0.name + ".params"; // generateMetadata(props) → props.params
124
+ } else if (p0.type === "ObjectPattern") {
125
+ const pp = p0.properties.find((x) => x.key && x.key.name === "params");
126
+ if (!pp) {
127
+ p0.properties.unshift(mkPat("({ params }) => 0").properties[0]); // { searchParams } → добавляем params
128
+ } else if (pp.value && pp.value.type === "Identifier") {
129
+ base = pp.value.name; // { params } или { params: p }
130
+ } else if (pp.value && pp.value.type === "ObjectPattern") {
131
+ // Вложенная деструктуризация { params: { id } } — сегменты как отдельные переменные;
132
+ // недостающие добавляем в паттерн, чтобы они были в области видимости.
133
+ for (const prop of pp.value.properties) if (prop.key) destructured.add(prop.key.name);
134
+ for (const seg of segNames) if (!destructured.has(seg)) {
135
+ pp.value.properties.push(mkPat(`({ ${seg} }) => 0`).properties[0]);
136
+ destructured.add(seg);
137
+ }
138
+ base = null; // используем голое имя для всех сегментов
139
+ }
140
+ }
141
+ for (const seg of segNames) {
142
+ if (base !== null && !destructured.has(seg)) {
143
+ effRoute = effRoute.replace("${" + seg + "}", "${" + base + "." + seg + "}");
144
+ }
117
145
  }
118
- }
119
-
120
- // Динамические параметры маршрута: если параметр деструктурирован (есть как
121
- // отдельная переменная) — используем его напрямую (${slug}); иначе — через
122
- // объект params (${params.slug}). Это чинит страницы, где обращаются как params.X.
123
- let effRoute = routeExpr;
124
- for (const m of [...routeExpr.matchAll(/\$\{(\w+)\}/g)]) {
125
- const name = m[1];
126
- const standalone = new RegExp("(?<!\\.)\\b" + name + "\\b").test(code);
127
- if (!standalone) effRoute = effRoute.replace("${" + name + "}", "${" + paramsBase + "." + name + "}");
128
146
  }
129
147
 
130
148
  const returns = collectReturns(fn.body, fn);
package/client.ts CHANGED
@@ -24,6 +24,7 @@ interface SeoFields {
24
24
  og?: { title?: string; description?: string; image?: string };
25
25
  twitter?: { card?: string; image?: string };
26
26
  jsonLd?: unknown[];
27
+ verification?: Record<string, string>; // проверочные мета-теги: metaName → content
27
28
  }
28
29
 
29
30
  async function fetchJson<T>(path: string, revalidate = 300): Promise<T | null> {
@@ -75,6 +76,11 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
75
76
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
76
77
  };
77
78
  }
79
+ // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
80
+ // Мержим, не затирая существующий other сайта.
81
+ if (f.verification && typeof f.verification === "object") {
82
+ m.other = { ...(m.other ?? {}), ...f.verification };
83
+ }
78
84
  return m;
79
85
  }
80
86
 
package/dist/client.js CHANGED
@@ -75,6 +75,11 @@ function mergeIntoMetadata(fallback, f) {
75
75
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
76
76
  };
77
77
  }
78
+ // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
79
+ // Мержим, не затирая существующий other сайта.
80
+ if (f.verification && typeof f.verification === "object") {
81
+ m.other = { ...(m.other ?? {}), ...f.verification };
82
+ }
78
83
  return m;
79
84
  }
80
85
  // В generateMetadata: const md = await seoMeta({ route, locale, fallback: compiled });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
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",