@webnumseoagent/next 0.1.17 → 0.1.19

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/client.ts CHANGED
@@ -20,7 +20,7 @@ interface SeoFields {
20
20
  title?: string;
21
21
  description?: string;
22
22
  canonical?: string;
23
- robots?: { index?: boolean; follow?: boolean };
23
+ robots?: { index?: boolean; follow?: boolean; noarchive?: boolean; nosnippet?: boolean; noimageindex?: boolean };
24
24
  og?: { title?: string; description?: string; image?: string; siteName?: string; locale?: string };
25
25
  twitter?: { card?: string; image?: string; site?: string };
26
26
  facebook?: { appId?: string; admins?: string }; // fb:app_id / fb:admins (admins — через запятую)
@@ -44,9 +44,30 @@ async function fetchJson<T>(path: string, revalidate = 300): Promise<T | null> {
44
44
  }
45
45
  }
46
46
 
47
+ // Next допускает robots и строкой ("noindex, nofollow"), и объектом. Чтобы не потерять
48
+ // строковый дефолт сайта при наложении директив, разбираем его в объект (токены точным
49
+ // сравнением — иначе "follow" совпал бы внутри "nofollow"). Незнакомые директивы (max-snippet…)
50
+ // опускаем — они редки, а оверрайд и так намеренный.
51
+ function robotsFromString(s: string): Record<string, unknown> {
52
+ const out: Record<string, unknown> = {};
53
+ for (const raw of s.split(",")) {
54
+ switch (raw.trim().toLowerCase()) {
55
+ case "noindex": out.index = false; break;
56
+ case "index": out.index = true; break;
57
+ case "nofollow": out.follow = false; break;
58
+ case "follow": out.follow = true; break;
59
+ case "noarchive": out.noarchive = true; break;
60
+ case "nosnippet": out.nosnippet = true; break;
61
+ case "noimageindex": out.noimageindex = true; break;
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+
47
67
  // Накладывает удалённый конфиг ПОВЕРХ скомпилированных дефолтов сайта.
48
- // robots: index флипается в false ТОЛЬКО если удалённо явно index:false; иначе
49
- // сохраняем дефолт сайта (защита от случайного деиндекса).
68
+ // robots: index/follow меняем ТОЛЬКО при явном булеве (иначе сохраняем дефолт сайта — защита
69
+ // от случайного деиндекса); ограничительные директивы (noarchive/nosnippet/noimageindex)
70
+ // добавляем поверх дефолта сайта только когда явно включены.
50
71
  function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
51
72
  const m: Metadata = { ...fallback };
52
73
  if (f.title) {
@@ -61,7 +82,17 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
61
82
  if (f.description) m.description = f.description;
62
83
  if (f.canonical) m.alternates = { ...(m.alternates ?? {}), canonical: f.canonical };
63
84
  if (f.robots) {
64
- m.robots = { index: f.robots.index !== false, follow: f.robots.follow !== false };
85
+ const r = f.robots;
86
+ const base: Record<string, unknown> =
87
+ typeof m.robots === "string" ? robotsFromString(m.robots)
88
+ : m.robots && typeof m.robots === "object" ? { ...(m.robots as Record<string, unknown>) }
89
+ : {};
90
+ if (typeof r.index === "boolean") base.index = r.index; // false → noindex; true → index; иначе не трогаем
91
+ if (typeof r.follow === "boolean") base.follow = r.follow;
92
+ if (r.noarchive) base.noarchive = true;
93
+ if (r.nosnippet) base.nosnippet = true;
94
+ if (r.noimageindex) base.noimageindex = true;
95
+ m.robots = base as Metadata["robots"];
65
96
  }
66
97
  if (f.og) {
67
98
  m.openGraph = {
package/dist/client.js CHANGED
@@ -39,9 +39,43 @@ async function fetchJson(path, revalidate = 300) {
39
39
  return null;
40
40
  }
41
41
  }
42
+ // Next допускает robots и строкой ("noindex, nofollow"), и объектом. Чтобы не потерять
43
+ // строковый дефолт сайта при наложении директив, разбираем его в объект (токены точным
44
+ // сравнением — иначе "follow" совпал бы внутри "nofollow"). Незнакомые директивы (max-snippet…)
45
+ // опускаем — они редки, а оверрайд и так намеренный.
46
+ function robotsFromString(s) {
47
+ const out = {};
48
+ for (const raw of s.split(",")) {
49
+ switch (raw.trim().toLowerCase()) {
50
+ case "noindex":
51
+ out.index = false;
52
+ break;
53
+ case "index":
54
+ out.index = true;
55
+ break;
56
+ case "nofollow":
57
+ out.follow = false;
58
+ break;
59
+ case "follow":
60
+ out.follow = true;
61
+ break;
62
+ case "noarchive":
63
+ out.noarchive = true;
64
+ break;
65
+ case "nosnippet":
66
+ out.nosnippet = true;
67
+ break;
68
+ case "noimageindex":
69
+ out.noimageindex = true;
70
+ break;
71
+ }
72
+ }
73
+ return out;
74
+ }
42
75
  // Накладывает удалённый конфиг ПОВЕРХ скомпилированных дефолтов сайта.
43
- // robots: index флипается в false ТОЛЬКО если удалённо явно index:false; иначе
44
- // сохраняем дефолт сайта (защита от случайного деиндекса).
76
+ // robots: index/follow меняем ТОЛЬКО при явном булеве (иначе сохраняем дефолт сайта — защита
77
+ // от случайного деиндекса); ограничительные директивы (noarchive/nosnippet/noimageindex)
78
+ // добавляем поверх дефолта сайта только когда явно включены.
45
79
  function mergeIntoMetadata(fallback, f) {
46
80
  const m = { ...fallback };
47
81
  if (f.title) {
@@ -59,7 +93,21 @@ function mergeIntoMetadata(fallback, f) {
59
93
  if (f.canonical)
60
94
  m.alternates = { ...(m.alternates ?? {}), canonical: f.canonical };
61
95
  if (f.robots) {
62
- m.robots = { index: f.robots.index !== false, follow: f.robots.follow !== false };
96
+ const r = f.robots;
97
+ const base = typeof m.robots === "string" ? robotsFromString(m.robots)
98
+ : m.robots && typeof m.robots === "object" ? { ...m.robots }
99
+ : {};
100
+ if (typeof r.index === "boolean")
101
+ base.index = r.index; // false → noindex; true → index; иначе не трогаем
102
+ if (typeof r.follow === "boolean")
103
+ base.follow = r.follow;
104
+ if (r.noarchive)
105
+ base.noarchive = true;
106
+ if (r.nosnippet)
107
+ base.nosnippet = true;
108
+ if (r.noimageindex)
109
+ base.noimageindex = true;
110
+ m.robots = base;
63
111
  }
64
112
  if (f.og) {
65
113
  m.openGraph = {
@@ -35,6 +35,30 @@ function escapeXml(s) {
35
35
  .replace(/"/g, "&quot;")
36
36
  .replace(/'/g, "&apos;");
37
37
  }
38
+ // Авто-репорт списка страниц на платформу: у адаптера есть ВСЕ URL сайта (из генератора
39
+ // карты), поэтому он сам отдаёт их платформе — там появляется полный список «SEO страниц»,
40
+ // и новые страницы подхватываются сами (без ручного скана). Fail-safe + троттлинг.
41
+ let lastReport = 0;
42
+ async function reportPages(entries) {
43
+ if (!API || !SITE || !TOKEN)
44
+ return;
45
+ const now = Date.now();
46
+ if (now - lastReport < 60000)
47
+ return; // не чаще раза в минуту на процесс
48
+ lastReport = now;
49
+ try {
50
+ await fetch(`${API}/api/seo/${SITE}/pages`, {
51
+ method: "POST",
52
+ headers: { "x-seo-agent-key": TOKEN, "content-type": "application/json" },
53
+ body: JSON.stringify({ pages: entries.map((e) => ({ loc: e.loc, lastmod: e.lastmod, title: e.title })) }),
54
+ signal: AbortSignal.timeout(4000),
55
+ cache: "no-store",
56
+ });
57
+ }
58
+ catch {
59
+ /* fail-safe: не критично — обновится при следующей генерации карты */
60
+ }
61
+ }
38
62
  function normalize(entries) {
39
63
  const out = [];
40
64
  for (const e of entries) {
@@ -46,12 +70,14 @@ function normalize(entries) {
46
70
  const images = Array.isArray(e.images)
47
71
  ? e.images.filter((i) => typeof i === "string" && !!i)
48
72
  : [];
73
+ const title = typeof e.title === "string" ? e.title : typeof e.name === "string" ? e.name : undefined;
49
74
  out.push({
50
75
  loc: e.url,
51
76
  lastmod,
52
77
  changefreq: typeof e.changeFrequency === "string" ? e.changeFrequency : undefined,
53
78
  priority: typeof e.priority === "number" ? e.priority : undefined,
54
79
  images: images.length ? images : undefined,
80
+ title: title?.trim() || undefined,
55
81
  });
56
82
  }
57
83
  return out;
@@ -148,6 +174,10 @@ async function seoWrapSitemap(fn, kind, group) {
148
174
  const origin = originOf(entries);
149
175
  if (!origin || entries.length === 0)
150
176
  return EMPTY;
177
+ // Отдаём платформе ПОЛНЫЙ список страниц (до исключений) — только на индексном/плоском
178
+ // вызове (у него все URL). Await + троттлинг: доставка надёжна, задержка редка (карта кэш 300с).
179
+ if (kind === "index" || kind === "flat")
180
+ await reportPages(entries);
151
181
  // Трансформации применяем к списку записей ОДИН раз, до ветвления по kind —
152
182
  // иначе flat и «схлопывание в один раздел» их обходят.
153
183
  if (!cfg.images)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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",
package/sitemap-build.ts CHANGED
@@ -54,6 +54,29 @@ interface NEntry {
54
54
  changefreq?: string;
55
55
  priority?: number;
56
56
  images?: string[];
57
+ title?: string; // имя записи (name/title из генератора) — для списка «SEO страниц» на платформе
58
+ }
59
+
60
+ // Авто-репорт списка страниц на платформу: у адаптера есть ВСЕ URL сайта (из генератора
61
+ // карты), поэтому он сам отдаёт их платформе — там появляется полный список «SEO страниц»,
62
+ // и новые страницы подхватываются сами (без ручного скана). Fail-safe + троттлинг.
63
+ let lastReport = 0;
64
+ async function reportPages(entries: NEntry[]): Promise<void> {
65
+ if (!API || !SITE || !TOKEN) return;
66
+ const now = Date.now();
67
+ if (now - lastReport < 60_000) return; // не чаще раза в минуту на процесс
68
+ lastReport = now;
69
+ try {
70
+ await fetch(`${API}/api/seo/${SITE}/pages`, {
71
+ method: "POST",
72
+ headers: { "x-seo-agent-key": TOKEN, "content-type": "application/json" },
73
+ body: JSON.stringify({ pages: entries.map((e) => ({ loc: e.loc, lastmod: e.lastmod, title: e.title })) }),
74
+ signal: AbortSignal.timeout(4000),
75
+ cache: "no-store",
76
+ });
77
+ } catch {
78
+ /* fail-safe: не критично — обновится при следующей генерации карты */
79
+ }
57
80
  }
58
81
 
59
82
  // Запись карты может нести доп. поля (напр. images) сверх типа Next — читаем через каст.
@@ -63,6 +86,8 @@ type RawEntry = {
63
86
  changeFrequency?: string;
64
87
  priority?: number;
65
88
  images?: unknown;
89
+ name?: unknown; // доп. поле генератора — имя записи (товар/категория/статья)
90
+ title?: unknown;
66
91
  };
67
92
 
68
93
  function normalize(entries: MetadataRoute.Sitemap): NEntry[] {
@@ -74,12 +99,14 @@ function normalize(entries: MetadataRoute.Sitemap): NEntry[] {
74
99
  const images = Array.isArray(e.images)
75
100
  ? e.images.filter((i: unknown): i is string => typeof i === "string" && !!i)
76
101
  : [];
102
+ const title = typeof e.title === "string" ? e.title : typeof e.name === "string" ? e.name : undefined;
77
103
  out.push({
78
104
  loc: e.url,
79
105
  lastmod,
80
106
  changefreq: typeof e.changeFrequency === "string" ? e.changeFrequency : undefined,
81
107
  priority: typeof e.priority === "number" ? e.priority : undefined,
82
108
  images: images.length ? images : undefined,
109
+ title: title?.trim() || undefined,
83
110
  });
84
111
  }
85
112
  return out;
@@ -180,6 +207,10 @@ export async function seoWrapSitemap(
180
207
  const origin = originOf(entries);
181
208
  if (!origin || entries.length === 0) return EMPTY;
182
209
 
210
+ // Отдаём платформе ПОЛНЫЙ список страниц (до исключений) — только на индексном/плоском
211
+ // вызове (у него все URL). Await + троттлинг: доставка надёжна, задержка редка (карта кэш 300с).
212
+ if (kind === "index" || kind === "flat") await reportPages(entries);
213
+
183
214
  // Трансформации применяем к списку записей ОДИН раз, до ветвления по kind —
184
215
  // иначе flat и «схлопывание в один раздел» их обходят.
185
216
  if (!cfg.images) for (const e of entries) e.images = undefined; // убрать изображения