@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 +31 -2
- package/bin/wrap.mjs +48 -0
- package/package.json +1 -1
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
|
-
|
|
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 {
|