@webnumseoagent/next 0.2.3 → 0.3.1
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 +162 -3
- package/client.ts +8 -2
- package/dist/client.js +7 -2
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -35,6 +35,19 @@ async function ensureFile(p, content) {
|
|
|
35
35
|
ok(path.relative(root, p));
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
+
async function anyExistsRel(paths) {
|
|
39
|
+
for (const p of paths) if (await exists(path.join(root, p))) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Переименовывает файл в *.bak (не удаляя) — сохраняем чужой robots/sitemap при takeover.
|
|
43
|
+
async function backupIfExists(p) {
|
|
44
|
+
if (!(await exists(p))) return false;
|
|
45
|
+
let bak = `${p}.bak`, i = 1;
|
|
46
|
+
while (await exists(bak)) bak = `${p}.bak${i++}`;
|
|
47
|
+
await fs.rename(p, bak);
|
|
48
|
+
ok(`${path.relative(root, p)} → ${path.relative(root, bak)} (бэкап)`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
38
51
|
|
|
39
52
|
async function findAppDir() {
|
|
40
53
|
for (const c of ["app", "src/app"]) {
|
|
@@ -469,10 +482,156 @@ async function findMetadataPages(dir, acc = []) {
|
|
|
469
482
|
return acc;
|
|
470
483
|
}
|
|
471
484
|
|
|
485
|
+
// takeover — передать robots.txt и/или sitemap.xml под управление платформы, даже если у сайта
|
|
486
|
+
// уже есть свои (статический public/robots.txt|sitemap.xml или app/robots.ts|sitemap.ts). Чужие
|
|
487
|
+
// файлы бэкапим в *.bak и ставим наши динамические роуты (их отдаёт адаптер, контентом рулит платформа).
|
|
488
|
+
// Флаги: --robots и/или --sitemap (без флагов — оба). Требует, чтобы адаптер уже был установлен (init).
|
|
489
|
+
async function takeover() {
|
|
490
|
+
log("\n\x1b[1m@webnumseoagent/next — takeover robots/sitemap\x1b[0m\n");
|
|
491
|
+
if (!isGitClean() && !args.includes("--force")) {
|
|
492
|
+
console.error(" ✗ Рабочее дерево git не чистое. Закоммить/откатись (или добавь --force), чтобы дифф был обозрим.");
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
const appDir = await findAppDir();
|
|
496
|
+
if (!appDir) {
|
|
497
|
+
console.error(" ✗ Не найдена папка app/ или src/app/. Запусти из корня Next.js-проекта.");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
ok(`Найден app-каталог: ${appDir}/`);
|
|
501
|
+
const ext = (await exists(path.join(root, "tsconfig.json"))) ? "ts" : "js";
|
|
502
|
+
|
|
503
|
+
// Импорт клиента: пакет установлен → из пакета; иначе — из вендор-папки seoagent/ (её создаёт init).
|
|
504
|
+
const pkgMode = await exists(path.join(root, "node_modules/@webnumseoagent/next/package.json"));
|
|
505
|
+
if (!pkgMode && !(await exists(path.join(root, "seoagent", "client.ts")))) {
|
|
506
|
+
console.error(" ✗ Адаптер не установлен (нет пакета и нет seoagent/client.ts). Сначала запусти init.");
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
const clientFor = (file) => (pkgMode ? "@webnumseoagent/next" : `${importPath(appDir, file)}/client`);
|
|
510
|
+
|
|
511
|
+
// Без флагов — оба; иначе только выбранные.
|
|
512
|
+
const only = args.includes("--robots") || args.includes("--sitemap");
|
|
513
|
+
const doRobots = !only || args.includes("--robots");
|
|
514
|
+
const doSitemap = !only || args.includes("--sitemap");
|
|
515
|
+
|
|
516
|
+
const hdr = `{ "content-type": "application/xml; charset=utf-8", "x-seoagent-sitemap": "1" }`;
|
|
517
|
+
const childSig = ext === "ts" ? "_req: Request, { params }: { params: { name: string } }" : "_req, { params }";
|
|
518
|
+
|
|
519
|
+
if (doRobots) {
|
|
520
|
+
if (await anyExistsRel([`${appDir}/robots.txt/route.ts`, `${appDir}/robots.txt/route.js`])) {
|
|
521
|
+
skip("robots (наш роут уже есть)");
|
|
522
|
+
} else {
|
|
523
|
+
// Бэкапим чужой robots: статический public/robots.txt, метаданные app/robots.ts|js И статический
|
|
524
|
+
// app/robots.txt (Next-конвенция). Последний критично отодвинуть: иначе mkdir("app/robots.txt")
|
|
525
|
+
// для нашего роут-каталога упадёт с EEXIST (по этому пути уже лежит ФАЙЛ).
|
|
526
|
+
await backupIfExists(path.join(root, "public", "robots.txt"));
|
|
527
|
+
await backupIfExists(path.join(root, appDir, "robots.ts"));
|
|
528
|
+
await backupIfExists(path.join(root, appDir, "robots.js"));
|
|
529
|
+
await backupIfExists(path.join(root, appDir, "robots.txt"));
|
|
530
|
+
await ensureFile(
|
|
531
|
+
path.join(root, appDir, `robots.txt/route.${ext}`),
|
|
532
|
+
`import { seoRobotsTxt } from "${clientFor("robots.txt/route.ts")}";\nexport const revalidate = 300;\nexport async function GET() {\n return new Response(await seoRobotsTxt(), { headers: { "content-type": "text/plain; charset=utf-8", "x-seoagent-robots": "1" } });\n}\n`,
|
|
533
|
+
);
|
|
534
|
+
ok("robots.txt → под управлением платформы");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (doSitemap) {
|
|
539
|
+
if (await anyExistsRel([`${appDir}/sitemap.xml/route.ts`, `${appDir}/sitemap.xml/route.js`])) {
|
|
540
|
+
skip("sitemap (наш роут уже есть)");
|
|
541
|
+
} else {
|
|
542
|
+
const xslRoute = () => ensureFile(
|
|
543
|
+
path.join(root, appDir, `sitemap.xsl/route.${ext}`),
|
|
544
|
+
`import { SITEMAP_XSL } from "${clientFor("sitemap.xsl/route.ts")}";\nexport function GET() {\n return new Response(SITEMAP_XSL, { headers: { "content-type": "text/xsl; charset=utf-8" } });\n}\n`,
|
|
545
|
+
);
|
|
546
|
+
// Конфликт: у сайта уже есть app/sitemap/[иной-сегмент] — тогда свой [name] не создаём.
|
|
547
|
+
let dynConflict = false;
|
|
548
|
+
const smDir = path.join(root, appDir, "sitemap");
|
|
549
|
+
if (await exists(smDir)) {
|
|
550
|
+
const names = await fs.readdir(smDir).catch(() => []);
|
|
551
|
+
dynConflict = names.some((n) => /^\[.*\]$/.test(n) && n !== "[name]");
|
|
552
|
+
}
|
|
553
|
+
// Роуты-обёртки (зовут переименованную карту сайта sitemap.source + добавляют стиль/разбивку).
|
|
554
|
+
const genWrapRoutes = async () => {
|
|
555
|
+
await ensureFile(
|
|
556
|
+
path.join(root, appDir, `sitemap.xml/route.${ext}`),
|
|
557
|
+
`import original from "../sitemap.source";\nimport { seoWrapSitemap } from "${clientFor("sitemap.xml/route.ts")}";\nexport const revalidate = 300;\nexport async function GET() {\n return new Response(await seoWrapSitemap(original, "${dynConflict ? "flat" : "index"}"), { headers: ${hdr} });\n}\n`,
|
|
558
|
+
);
|
|
559
|
+
if (!dynConflict)
|
|
560
|
+
await ensureFile(
|
|
561
|
+
path.join(root, appDir, `sitemap/[name]/route.${ext}`),
|
|
562
|
+
`import original from "../../sitemap.source";\nimport { seoWrapSitemap } from "${clientFor("sitemap/[name]/route.ts")}";\nexport const revalidate = 300;\nexport async function GET(${childSig}) {\n return new Response(await seoWrapSitemap(original, "child", params.name), { headers: ${hdr} });\n}\n`,
|
|
563
|
+
);
|
|
564
|
+
await xslRoute();
|
|
565
|
+
};
|
|
566
|
+
// Роуты-генерации (карту собирает платформа).
|
|
567
|
+
const genGenerateRoutes = async () => {
|
|
568
|
+
await ensureFile(
|
|
569
|
+
path.join(root, appDir, `sitemap.xml/route.${ext}`),
|
|
570
|
+
`import { seoSitemapXml } from "${clientFor("sitemap.xml/route.ts")}";\nexport const revalidate = 300;\nexport async function GET() {\n return new Response(await seoSitemapXml("/sitemap.xml"), { headers: ${hdr} });\n}\n`,
|
|
571
|
+
);
|
|
572
|
+
if (!dynConflict)
|
|
573
|
+
await ensureFile(
|
|
574
|
+
path.join(root, appDir, `sitemap/[name]/route.${ext}`),
|
|
575
|
+
`import { seoSitemapXml } from "${clientFor("sitemap/[name]/route.ts")}";\nexport const revalidate = 300;\nexport async function GET(${childSig}) {\n return new Response(await seoSitemapXml("/sitemap/" + params.name), { headers: ${hdr} });\n}\n`,
|
|
576
|
+
);
|
|
577
|
+
await xslRoute();
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Возобновление прерванной обёртки: карта уже переименована в sitemap.source.* (а роутов ещё нет) —
|
|
581
|
+
// просто доделываем wrap-роуты. Иначе повторный запуск ушёл бы в «генерацию» и осиротил бы URL сайта.
|
|
582
|
+
let sourceExt = null;
|
|
583
|
+
for (const e of ["ts", "js", "tsx", "jsx"]) {
|
|
584
|
+
if (await exists(path.join(root, appDir, `sitemap.source.${e}`))) { sourceExt = e; break; }
|
|
585
|
+
}
|
|
586
|
+
if (sourceExt) {
|
|
587
|
+
await genWrapRoutes();
|
|
588
|
+
ok("sitemap.xml → под управлением платформы (обёртка — докрутка после прерывания)");
|
|
589
|
+
} else {
|
|
590
|
+
// Динамическая карта сайта (app/sitemap.ts|js) со СВОИМИ URL: не теряем её — оборачиваем.
|
|
591
|
+
// Только стандартный default-export без generateSitemaps.
|
|
592
|
+
let metaSitemap = null;
|
|
593
|
+
for (const e of ["ts", "js", "tsx", "jsx"]) {
|
|
594
|
+
const p = path.join(root, appDir, `sitemap.${e}`);
|
|
595
|
+
if (await exists(p)) { metaSitemap = { p, e }; break; }
|
|
596
|
+
}
|
|
597
|
+
let wrap = false;
|
|
598
|
+
if (metaSitemap) {
|
|
599
|
+
const code = await fs.readFile(metaSitemap.p, "utf8");
|
|
600
|
+
wrap = !/\bgenerateSitemaps\b/.test(code) &&
|
|
601
|
+
(/export\s+default\s+(async\s+)?function/.test(code) || /export\s+default\s+\w/.test(code));
|
|
602
|
+
}
|
|
603
|
+
// Чужую статическую карту (public/sitemap*.xml и статический app/sitemap.xml) бэкапим в любом
|
|
604
|
+
// случае — иначе она перекроет наш роут; app/sitemap.xml критично отодвинуть (иначе mkdir EEXIST).
|
|
605
|
+
await backupIfExists(path.join(root, "public", "sitemap.xml"));
|
|
606
|
+
await backupIfExists(path.join(root, "public", "sitemap_index.xml"));
|
|
607
|
+
await backupIfExists(path.join(root, appDir, "sitemap.xml"));
|
|
608
|
+
if (wrap) {
|
|
609
|
+
await fs.rename(metaSitemap.p, path.join(root, appDir, `sitemap.source.${metaSitemap.e}`));
|
|
610
|
+
ok(`${appDir}/sitemap.${metaSitemap.e} → ${appDir}/sitemap.source.${metaSitemap.e} (оборачиваем существующую карту)`);
|
|
611
|
+
await genWrapRoutes();
|
|
612
|
+
ok("sitemap.xml → под управлением платформы (обёртка карты сайта)");
|
|
613
|
+
} else {
|
|
614
|
+
if (metaSitemap) await backupIfExists(metaSitemap.p); // нестандартный app/sitemap.* (напр. generateSitemaps)
|
|
615
|
+
await genGenerateRoutes();
|
|
616
|
+
ok("sitemap.xml → под управлением платформы (генерация с платформы)");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
log("\n\x1b[1mЗатем:\x1b[0m проверь \x1b[36mgit diff\x1b[0m, затем \x1b[36mgit add -A && git commit && git push\x1b[0m. Убедись, что env SEOAGENT_* заданы в Vercel.\n");
|
|
623
|
+
}
|
|
624
|
+
|
|
472
625
|
if (cmd === "init") {
|
|
473
626
|
init().catch((e) => { console.error(e); process.exit(1); });
|
|
627
|
+
} else if (cmd === "takeover") {
|
|
628
|
+
takeover().catch((e) => { console.error(e); process.exit(1); });
|
|
474
629
|
} else {
|
|
475
|
-
log("Использование: npx @webnumseoagent/next init
|
|
476
|
-
log(" --wrap
|
|
477
|
-
log("
|
|
630
|
+
log("Использование: npx @webnumseoagent/next <init|takeover> [опции]");
|
|
631
|
+
log(" init [--wrap] [--no-blog] [--site <id>] [--token <t>] [--api <url>] [--revalidate-secret <s>]");
|
|
632
|
+
log(" --wrap — авто-обернуть generateMetadata страниц (иначе печатает сниппеты)");
|
|
633
|
+
log(" --no-blog — не создавать роуты блога /blog (по умолчанию блог создаётся)");
|
|
634
|
+
log(" takeover [--robots] [--sitemap] [--force]");
|
|
635
|
+
log(" передать robots.txt/sitemap.xml под управление платформы (бэкап чужих в *.bak);");
|
|
636
|
+
log(" без флагов — оба. Требует уже установленного адаптера (init).");
|
|
478
637
|
}
|
package/client.ts
CHANGED
|
@@ -303,8 +303,14 @@ export async function seoRobots(): Promise<MetadataRoute.Robots> {
|
|
|
303
303
|
// return new Response(await seoRobotsTxt(), { headers: { "content-type": "text/plain" } });
|
|
304
304
|
// }
|
|
305
305
|
// Fail-safe: при недоступности платформы отдаём разрешающий robots (никогда не Disallow: /).
|
|
306
|
+
// Маркер-комментарий в начале robots.txt — чтобы платформа могла определить, что robots отдаёт
|
|
307
|
+
// наш адаптер, даже когда Vercel обслуживает роут как ISR-статик и срезает заголовок x-seoagent-robots
|
|
308
|
+
// (по телу — надёжно). Комментарий безвреден: краулеры игнорируют строки, начинающиеся с #.
|
|
309
|
+
const ROBOTS_MARK = "# SEO Agent\n";
|
|
310
|
+
const withRobotsMark = (t: string) => (t.startsWith(ROBOTS_MARK) ? t : ROBOTS_MARK + t);
|
|
311
|
+
|
|
306
312
|
export async function seoRobotsTxt(): Promise<string> {
|
|
307
|
-
const FALLBACK = "User-agent: *\nAllow: /\n";
|
|
313
|
+
const FALLBACK = withRobotsMark("User-agent: *\nAllow: /\n");
|
|
308
314
|
if (!API || !SITE || !TOKEN) return FALLBACK;
|
|
309
315
|
try {
|
|
310
316
|
const r = await fetch(`${API}/api/seo/${SITE}/robots.txt`, {
|
|
@@ -313,7 +319,7 @@ export async function seoRobotsTxt(): Promise<string> {
|
|
|
313
319
|
signal: AbortSignal.timeout(5000),
|
|
314
320
|
});
|
|
315
321
|
if (!r.ok) return FALLBACK;
|
|
316
|
-
return await r.text();
|
|
322
|
+
return withRobotsMark(await r.text());
|
|
317
323
|
} catch {
|
|
318
324
|
return FALLBACK;
|
|
319
325
|
}
|
package/dist/client.js
CHANGED
|
@@ -305,8 +305,13 @@ async function seoRobots() {
|
|
|
305
305
|
// return new Response(await seoRobotsTxt(), { headers: { "content-type": "text/plain" } });
|
|
306
306
|
// }
|
|
307
307
|
// Fail-safe: при недоступности платформы отдаём разрешающий robots (никогда не Disallow: /).
|
|
308
|
+
// Маркер-комментарий в начале robots.txt — чтобы платформа могла определить, что robots отдаёт
|
|
309
|
+
// наш адаптер, даже когда Vercel обслуживает роут как ISR-статик и срезает заголовок x-seoagent-robots
|
|
310
|
+
// (по телу — надёжно). Комментарий безвреден: краулеры игнорируют строки, начинающиеся с #.
|
|
311
|
+
const ROBOTS_MARK = "# SEO Agent\n";
|
|
312
|
+
const withRobotsMark = (t) => (t.startsWith(ROBOTS_MARK) ? t : ROBOTS_MARK + t);
|
|
308
313
|
async function seoRobotsTxt() {
|
|
309
|
-
const FALLBACK = "User-agent: *\nAllow: /\n";
|
|
314
|
+
const FALLBACK = withRobotsMark("User-agent: *\nAllow: /\n");
|
|
310
315
|
if (!API || !SITE || !TOKEN)
|
|
311
316
|
return FALLBACK;
|
|
312
317
|
try {
|
|
@@ -317,7 +322,7 @@ async function seoRobotsTxt() {
|
|
|
317
322
|
});
|
|
318
323
|
if (!r.ok)
|
|
319
324
|
return FALLBACK;
|
|
320
|
-
return await r.text();
|
|
325
|
+
return withRobotsMark(await r.text());
|
|
321
326
|
}
|
|
322
327
|
catch {
|
|
323
328
|
return FALLBACK;
|
package/package.json
CHANGED