@webnumseoagent/next 0.1.1 → 0.1.2

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
@@ -8,7 +8,7 @@ import { promises as fs } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { execSync } from "node:child_process";
11
- import { wrapFile } from "./wrap.mjs";
11
+ import { wrapFile, mountJsonLd } from "./wrap.mjs";
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const pkgRoot = path.resolve(__dirname, "..");
@@ -140,6 +140,7 @@ async function init() {
140
140
  // 6. generateMetadata: авто-обёртка (--wrap) или готовые сниппеты
141
141
  const pages = await findMetadataPages(path.join(root, appDir));
142
142
  const wrapMode = args.includes("--wrap");
143
+ let jsonLdMounted = false;
143
144
 
144
145
  if (wrapMode) {
145
146
  log("\n\x1b[1mАвто-обёртка generateMetadata:\x1b[0m");
@@ -161,6 +162,18 @@ async function init() {
161
162
  log(` \x1b[36mimport { seoMeta } from "${importClient}";\n return seoMeta({ route: ${m.expr}, locale, fallback: { /* прежний объект */ } });\x1b[0m`);
162
163
  }
163
164
  }
165
+
166
+ // Авто-монтирование <SeoJsonLd/> в layout (человек код не трогает).
167
+ // Отключить можно флагом --no-mount-jsonld.
168
+ if (!args.includes("--no-mount-jsonld")) {
169
+ const layouts = await findLayouts(path.join(root, appDir));
170
+ for (const lp of layouts) {
171
+ const code = await fs.readFile(lp, "utf8");
172
+ const r = await mountJsonLd(code, importJsonld);
173
+ if (r.ok) { await fs.writeFile(lp, r.code); ok(`${path.relative(root, lp)} → смонтирован <SeoJsonLd/>`); jsonLdMounted = true; }
174
+ else if (r.reason === "already") jsonLdMounted = true;
175
+ }
176
+ }
164
177
  } else {
165
178
  log(`\n \x1b[1mimport { seoMeta } from "${importClient}";\x1b[0m — добавь этот импорт в файлы ниже.`);
166
179
  log("\n\x1b[1mОсталось вручную: в каждой странице замени `return { …меты }` на строку из блока (route уже посчитан):\x1b[0m");
@@ -173,10 +186,26 @@ async function init() {
173
186
  }
174
187
  }
175
188
 
176
- log(`\n И смонтируй JSON-LD в layout (внутри <body>):\n \x1b[36mimport { SeoJsonLd } from "${importJsonld}";\n <SeoJsonLd route="/" />\x1b[0m`);
189
+ if (!jsonLdMounted) {
190
+ log(`\n И смонтируй JSON-LD в layout (внутри <body>):\n \x1b[36mimport { SeoJsonLd } from "${importJsonld}";\n <SeoJsonLd route="/" />\x1b[0m`);
191
+ }
177
192
  log("\n\x1b[1mЗатем:\x1b[0m задай эти же env-переменные в Vercel (Settings → Environment Variables) и сделай git push.\n");
178
193
  }
179
194
 
195
+ // Ищет файлы layout.(tsx|ts) под app/.
196
+ async function findLayouts(dir, acc = []) {
197
+ for (const e of await fs.readdir(dir, { withFileTypes: true })) {
198
+ const p = path.join(dir, e.name);
199
+ if (e.isDirectory()) {
200
+ if (e.name === "node_modules" || e.name === "api") continue;
201
+ await findLayouts(p, acc);
202
+ } else if (/^layout\.(tsx|ts)$/.test(e.name)) {
203
+ acc.push(p);
204
+ }
205
+ }
206
+ return acc;
207
+ }
208
+
180
209
  function isGitClean() {
181
210
  try {
182
211
  return execSync("git status --porcelain", { cwd: root, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() === "";
package/bin/wrap.mjs CHANGED
@@ -22,6 +22,54 @@ function collectReturns(fnBody, fn) {
22
22
  return out;
23
23
  }
24
24
 
25
+ // Монтирует <SeoJsonLd route="/" /> в <body> layout + добавляет импорт.
26
+ // Best-effort: если <body> не найден или уже смонтировано — {ok:false, reason}.
27
+ export async function mountJsonLd(code, importSpec) {
28
+ let recast, babelParser;
29
+ try {
30
+ recast = await import("recast");
31
+ babelParser = await import("@babel/parser");
32
+ } catch {
33
+ return { ok: false, reason: "deps" };
34
+ }
35
+ if (/\bSeoJsonLd\b/.test(code)) return { ok: false, reason: "already" };
36
+ const plugins = ["typescript", "jsx"];
37
+ const parser = { parse: (src) => babelParser.parse(src, { sourceType: "module", tokens: true, plugins }) };
38
+ let ast;
39
+ try { ast = recast.parse(code, { parser }); } catch { return { ok: false, reason: "parse" }; }
40
+
41
+ // Ищем JSXElement <body>.
42
+ let body = null;
43
+ (function walk(node) {
44
+ if (body || !node || typeof node !== "object") return;
45
+ if (Array.isArray(node)) return node.forEach(walk);
46
+ if (node.type === "JSXElement" && node.openingElement?.name?.name === "body") { body = node; return; }
47
+ for (const k in node) {
48
+ if (k === "loc" || k === "tokens" || k === "comments") continue;
49
+ const v = node[k];
50
+ if (v && typeof v === "object") walk(v);
51
+ }
52
+ })(ast.program);
53
+ if (!body) return { ok: false, reason: "no-body" };
54
+
55
+ let el;
56
+ try { el = babelParser.parseExpression(`<SeoJsonLd route="/" />`, { plugins }); } catch { return { ok: false, reason: "el" }; }
57
+ body.children.push(el);
58
+
59
+ const has = ast.program.body.some(
60
+ (n) => n.type === "ImportDeclaration" && n.source.value === importSpec && n.specifiers.some((s) => s.imported && s.imported.name === "SeoJsonLd"),
61
+ );
62
+ if (!has) {
63
+ const imp = babelParser.parse(`import { SeoJsonLd } from "${importSpec}";`, { sourceType: "module", plugins }).program.body[0];
64
+ ast.program.body.unshift(imp);
65
+ }
66
+
67
+ let out;
68
+ try { out = recast.print(ast).code; } catch { return { ok: false, reason: "print" }; }
69
+ try { babelParser.parse(out, { sourceType: "module", plugins }); } catch { return { ok: false, reason: "invalid-output" }; }
70
+ return { ok: true, code: out };
71
+ }
72
+
25
73
  export async function wrapFile(code, routeExpr, importSpec) {
26
74
  let recast, babelParser;
27
75
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Runtime SEO adapter + installer for Next.js App Router sites (SEO Agent platform)",
5
5
  "type": "module",
6
6
  "main": "dist/client.js",