@webnumseoagent/next 0.1.14 → 0.1.16

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
@@ -319,7 +319,7 @@ async function init() {
319
319
  log("\n Осталось вручную (замени `return { … }` на):");
320
320
  for (const m of manual) {
321
321
  log(` • ${path.relative(root, m.p)}`);
322
- 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`);
323
323
  }
324
324
  }
325
325
 
@@ -342,7 +342,7 @@ async function init() {
342
342
  const rel = path.relative(path.join(root, appDir), p);
343
343
  const { expr } = routeFromFile(rel);
344
344
  log(`\n • \x1b[1m${path.relative(root, p)}\x1b[0m`);
345
- log(` \x1b[36mreturn seoMeta({ route: ${expr}, locale, fallback: { /* твой прежний объект меты */ } });\x1b[0m`);
345
+ log(` \x1b[36mreturn seoMeta({ route: ${paramsExpr(expr)}, locale, fallback: { /* твой прежний объект меты */ } });\x1b[0m`);
346
346
  }
347
347
  }
348
348
 
@@ -382,14 +382,17 @@ function routeFromFile(relFromApp) {
382
382
  if (!segs.length) return { expr: '"/"', dynamic: false };
383
383
  let dynamic = false;
384
384
  const parts = segs.map((seg) => {
385
- const m = seg.match(/^\[(?:\.\.\.)?(.+)\]$/);
386
- 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, "") + "}"; }
387
387
  return seg;
388
388
  });
389
389
  const pathStr = "/" + parts.join("/");
390
390
  return { expr: dynamic ? "`" + pathStr + "`" : `"${pathStr}"`, dynamic };
391
391
  }
392
392
 
393
+ // Ручной сниппет вставляется в generateMetadata({ params }) — сегменты идут через params.
394
+ const paramsExpr = (expr) => expr.replace(/\$\{(\w+)\}/g, "${params.$1}");
395
+
393
396
  async function findMetadataPages(dir, acc = []) {
394
397
  for (const e of await fs.readdir(dir, { withFileTypes: true })) {
395
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
@@ -21,8 +21,9 @@ interface SeoFields {
21
21
  description?: string;
22
22
  canonical?: string;
23
23
  robots?: { index?: boolean; follow?: boolean };
24
- og?: { title?: string; description?: string; image?: string };
25
- twitter?: { card?: string; image?: string };
24
+ og?: { title?: string; description?: string; image?: string; siteName?: string; locale?: string };
25
+ twitter?: { card?: string; image?: string; site?: string };
26
+ facebook?: { appId?: string; admins?: string }; // fb:app_id / fb:admins (admins — через запятую)
26
27
  jsonLd?: unknown[];
27
28
  verification?: Record<string, string>; // проверочные мета-теги: metaName → content
28
29
  }
@@ -67,6 +68,8 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
67
68
  ...(f.og.title ? { title: f.og.title } : {}),
68
69
  ...(f.og.description ? { description: f.og.description } : {}),
69
70
  ...(f.og.image ? { images: [f.og.image] } : {}),
71
+ ...(f.og.siteName ? { siteName: f.og.siteName } : {}),
72
+ ...(f.og.locale ? { locale: f.og.locale } : {}),
70
73
  };
71
74
  }
72
75
  if (f.twitter) {
@@ -74,8 +77,19 @@ function mergeIntoMetadata(fallback: Metadata, f: SeoFields): Metadata {
74
77
  ...(m.twitter ?? {}),
75
78
  ...(f.twitter.card ? { card: f.twitter.card as "summary" | "summary_large_image" } : {}),
76
79
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
80
+ ...(f.twitter.site ? { site: f.twitter.site } : {}),
77
81
  };
78
82
  }
83
+ // fb:app_id / fb:admins — только через нативный metadata.facebook (property=, не name=).
84
+ // Тип Next — union (appId XOR admins), поэтому собираем объект и приводим к типу.
85
+ if (f.facebook && (f.facebook.appId || f.facebook.admins)) {
86
+ const admins = (f.facebook.admins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
87
+ m.facebook = {
88
+ ...(m.facebook ?? {}), // как и остальные блоки — накладываемся поверх дефолта сайта, не затирая его
89
+ ...(f.facebook.appId ? { appId: f.facebook.appId } : {}),
90
+ ...(admins.length ? { admins: admins.length === 1 ? admins[0] : admins } : {}),
91
+ } as Metadata["facebook"];
92
+ }
79
93
  // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
80
94
  // Мержим, не затирая существующий other сайта.
81
95
  if (f.verification && typeof f.verification === "object") {
package/dist/client.js CHANGED
@@ -66,6 +66,8 @@ function mergeIntoMetadata(fallback, f) {
66
66
  ...(f.og.title ? { title: f.og.title } : {}),
67
67
  ...(f.og.description ? { description: f.og.description } : {}),
68
68
  ...(f.og.image ? { images: [f.og.image] } : {}),
69
+ ...(f.og.siteName ? { siteName: f.og.siteName } : {}),
70
+ ...(f.og.locale ? { locale: f.og.locale } : {}),
69
71
  };
70
72
  }
71
73
  if (f.twitter) {
@@ -73,6 +75,17 @@ function mergeIntoMetadata(fallback, f) {
73
75
  ...(m.twitter ?? {}),
74
76
  ...(f.twitter.card ? { card: f.twitter.card } : {}),
75
77
  ...(f.twitter.image ? { images: [f.twitter.image] } : {}),
78
+ ...(f.twitter.site ? { site: f.twitter.site } : {}),
79
+ };
80
+ }
81
+ // fb:app_id / fb:admins — только через нативный metadata.facebook (property=, не name=).
82
+ // Тип Next — union (appId XOR admins), поэтому собираем объект и приводим к типу.
83
+ if (f.facebook && (f.facebook.appId || f.facebook.admins)) {
84
+ const admins = (f.facebook.admins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
85
+ m.facebook = {
86
+ ...(m.facebook ?? {}), // как и остальные блоки — накладываемся поверх дефолта сайта, не затирая его
87
+ ...(f.facebook.appId ? { appId: f.facebook.appId } : {}),
88
+ ...(admins.length ? { admins: admins.length === 1 ? admins[0] : admins } : {}),
76
89
  };
77
90
  }
78
91
  // Проверочные мета-теги веб-мастерских → <meta name content> через metadata.other.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webnumseoagent/next",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",