@webnumseoagent/next 0.1.16 → 0.1.18
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/analytics.tsx +73 -0
- package/client.ts +9 -0
- package/dist/analytics.d.ts +4 -0
- package/dist/analytics.js +38 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +6 -0
- package/dist/sitemap-build.js +30 -0
- package/package.json +6 -1
- package/sitemap-build.ts +31 -0
package/analytics.tsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Серверный компонент: вставляет счётчики аналитики (GA4 / Google Tag Manager / Яндекс.Метрика).
|
|
2
|
+
// Скрипты рендерятся в SSR-HTML и выполняются браузером при загрузке. Смонтируй ОДИН раз в layout:
|
|
3
|
+
// import { SeoAnalytics } from "@webnumseoagent/next/analytics";
|
|
4
|
+
// <body>… <SeoAnalytics /> …</body>
|
|
5
|
+
// Fail-safe: платформа недоступна → seoAnalytics() вернёт {} → ничего не рендерим.
|
|
6
|
+
import { seoAnalytics } from "./client";
|
|
7
|
+
|
|
8
|
+
// ID валидируются на платформе (saveAnalytics), но санитайзим и здесь (defense-in-depth):
|
|
9
|
+
// значения попадают в inline-скрипт, поэтому пропускаем только ожидаемый набор символов.
|
|
10
|
+
// typeof-гард: /config — недоверенный источник, нестроковое значение НЕ должно ронять рендер.
|
|
11
|
+
const ga4Id = (s?: string) => (typeof s === "string" && /^G-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
|
|
12
|
+
const gtmId = (s?: string) => (typeof s === "string" && /^GTM-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
|
|
13
|
+
// Метрика вставляется как ЧИСЛОВОЙ литерал — срезаем ведущие нули (иначе sloppy-mode парсит как octal).
|
|
14
|
+
const ymId = (s?: string) => (typeof s === "string" && /^\d+$/.test(s.trim()) ? s.trim().replace(/^0+(?=\d)/, "") : "");
|
|
15
|
+
|
|
16
|
+
export async function SeoAnalytics({ route, locale }: { route?: string; locale?: string } = {}) {
|
|
17
|
+
const a = await seoAnalytics(route ?? "/", locale);
|
|
18
|
+
const ga4 = ga4Id(a.ga4);
|
|
19
|
+
const gtm = gtmId(a.gtm);
|
|
20
|
+
const ym = ymId(a.metrica);
|
|
21
|
+
if (!ga4 && !gtm && !ym) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{gtm && (
|
|
26
|
+
<>
|
|
27
|
+
<script
|
|
28
|
+
// eslint-disable-next-line react/no-danger
|
|
29
|
+
dangerouslySetInnerHTML={{
|
|
30
|
+
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtm}');`,
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
<noscript>
|
|
34
|
+
<iframe
|
|
35
|
+
src={`https://www.googletagmanager.com/ns.html?id=${gtm}`}
|
|
36
|
+
height="0"
|
|
37
|
+
width="0"
|
|
38
|
+
style={{ display: "none", visibility: "hidden" }}
|
|
39
|
+
/>
|
|
40
|
+
</noscript>
|
|
41
|
+
</>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
{ga4 && (
|
|
45
|
+
<>
|
|
46
|
+
<script async src={`https://www.googletagmanager.com/gtag/js?id=${ga4}`} />
|
|
47
|
+
<script
|
|
48
|
+
// eslint-disable-next-line react/no-danger
|
|
49
|
+
dangerouslySetInnerHTML={{
|
|
50
|
+
__html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{ym && (
|
|
57
|
+
<>
|
|
58
|
+
<script
|
|
59
|
+
// eslint-disable-next-line react/no-danger
|
|
60
|
+
dangerouslySetInnerHTML={{
|
|
61
|
+
__html: `(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};m[i].l=1*new Date();for(var j=0;j<document.scripts.length;j++){if(document.scripts[j].src===r){return;}}k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})(window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym");ym(${ym},"init",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true});`,
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
<noscript>
|
|
65
|
+
<div>
|
|
66
|
+
<img src={`https://mc.yandex.ru/watch/${ym}`} style={{ position: "absolute", left: "-9999px" }} alt="" />
|
|
67
|
+
</div>
|
|
68
|
+
</noscript>
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
package/client.ts
CHANGED
|
@@ -26,6 +26,7 @@ interface SeoFields {
|
|
|
26
26
|
facebook?: { appId?: string; admins?: string }; // fb:app_id / fb:admins (admins — через запятую)
|
|
27
27
|
jsonLd?: unknown[];
|
|
28
28
|
verification?: Record<string, string>; // проверочные мета-теги: metaName → content
|
|
29
|
+
analytics?: { ga4?: string; gtm?: string; metrica?: string }; // счётчики (см. analytics.tsx)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
async function fetchJson<T>(path: string, revalidate = 300): Promise<T | null> {
|
|
@@ -182,3 +183,11 @@ export async function seoJsonLd(route: string, locale?: string): Promise<unknown
|
|
|
182
183
|
);
|
|
183
184
|
return data?.fields?.jsonLd ?? [];
|
|
184
185
|
}
|
|
186
|
+
|
|
187
|
+
// Счётчики аналитики (общесайтовые). Читаются в analytics.tsx (<SeoAnalytics/>).
|
|
188
|
+
export async function seoAnalytics(route = "/", locale?: string): Promise<{ ga4?: string; gtm?: string; metrica?: string }> {
|
|
189
|
+
const data = await fetchJson<{ fields: SeoFields }>(
|
|
190
|
+
`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`,
|
|
191
|
+
);
|
|
192
|
+
return data?.fields?.analytics ?? {};
|
|
193
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SeoAnalytics = SeoAnalytics;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
// Серверный компонент: вставляет счётчики аналитики (GA4 / Google Tag Manager / Яндекс.Метрика).
|
|
6
|
+
// Скрипты рендерятся в SSR-HTML и выполняются браузером при загрузке. Смонтируй ОДИН раз в layout:
|
|
7
|
+
// import { SeoAnalytics } from "@webnumseoagent/next/analytics";
|
|
8
|
+
// <body>… <SeoAnalytics /> …</body>
|
|
9
|
+
// Fail-safe: платформа недоступна → seoAnalytics() вернёт {} → ничего не рендерим.
|
|
10
|
+
const client_1 = require("./client");
|
|
11
|
+
// ID валидируются на платформе (saveAnalytics), но санитайзим и здесь (defense-in-depth):
|
|
12
|
+
// значения попадают в inline-скрипт, поэтому пропускаем только ожидаемый набор символов.
|
|
13
|
+
// typeof-гард: /config — недоверенный источник, нестроковое значение НЕ должно ронять рендер.
|
|
14
|
+
const ga4Id = (s) => (typeof s === "string" && /^G-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
|
|
15
|
+
const gtmId = (s) => (typeof s === "string" && /^GTM-[A-Z0-9]+$/i.test(s.trim()) ? s.trim() : "");
|
|
16
|
+
// Метрика вставляется как ЧИСЛОВОЙ литерал — срезаем ведущие нули (иначе sloppy-mode парсит как octal).
|
|
17
|
+
const ymId = (s) => (typeof s === "string" && /^\d+$/.test(s.trim()) ? s.trim().replace(/^0+(?=\d)/, "") : "");
|
|
18
|
+
async function SeoAnalytics({ route, locale } = {}) {
|
|
19
|
+
const a = await (0, client_1.seoAnalytics)(route ?? "/", locale);
|
|
20
|
+
const ga4 = ga4Id(a.ga4);
|
|
21
|
+
const gtm = gtmId(a.gtm);
|
|
22
|
+
const ym = ymId(a.metrica);
|
|
23
|
+
if (!ga4 && !gtm && !ym)
|
|
24
|
+
return null;
|
|
25
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [gtm && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", {
|
|
26
|
+
// eslint-disable-next-line react/no-danger
|
|
27
|
+
dangerouslySetInnerHTML: {
|
|
28
|
+
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtm}');`,
|
|
29
|
+
} }), (0, jsx_runtime_1.jsx)("noscript", { children: (0, jsx_runtime_1.jsx)("iframe", { src: `https://www.googletagmanager.com/ns.html?id=${gtm}`, height: "0", width: "0", style: { display: "none", visibility: "hidden" } }) })] })), ga4 && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${ga4}` }), (0, jsx_runtime_1.jsx)("script", {
|
|
30
|
+
// eslint-disable-next-line react/no-danger
|
|
31
|
+
dangerouslySetInnerHTML: {
|
|
32
|
+
__html: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${ga4}');`,
|
|
33
|
+
} })] })), ym && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", {
|
|
34
|
+
// eslint-disable-next-line react/no-danger
|
|
35
|
+
dangerouslySetInnerHTML: {
|
|
36
|
+
__html: `(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};m[i].l=1*new Date();for(var j=0;j<document.scripts.length;j++){if(document.scripts[j].src===r){return;}}k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})(window,document,"script","https://mc.yandex.ru/metrika/tag.js","ym");ym(${ym},"init",{clickmap:true,trackLinks:true,accurateTrackBounce:true,webvisor:true});`,
|
|
37
|
+
} }), (0, jsx_runtime_1.jsx)("noscript", { children: (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("img", { src: `https://mc.yandex.ru/watch/${ym}`, style: { position: "absolute", left: "-9999px" }, alt: "" }) }) })] }))] }));
|
|
38
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -11,3 +11,8 @@ export declare function seoSitemap(): Promise<MetadataRoute.Sitemap>;
|
|
|
11
11
|
export declare function seoRobots(): Promise<MetadataRoute.Robots>;
|
|
12
12
|
export declare function seoRobotsTxt(): Promise<string>;
|
|
13
13
|
export declare function seoJsonLd(route: string, locale?: string): Promise<unknown[]>;
|
|
14
|
+
export declare function seoAnalytics(route?: string, locale?: string): Promise<{
|
|
15
|
+
ga4?: string;
|
|
16
|
+
gtm?: string;
|
|
17
|
+
metrica?: string;
|
|
18
|
+
}>;
|
package/dist/client.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.seoSitemap = seoSitemap;
|
|
|
18
18
|
exports.seoRobots = seoRobots;
|
|
19
19
|
exports.seoRobotsTxt = seoRobotsTxt;
|
|
20
20
|
exports.seoJsonLd = seoJsonLd;
|
|
21
|
+
exports.seoAnalytics = seoAnalytics;
|
|
21
22
|
const API = process.env.SEOAGENT_API_BASE ?? "";
|
|
22
23
|
const SITE = process.env.SEOAGENT_SITE_ID ?? "";
|
|
23
24
|
const TOKEN = process.env.SEOAGENT_TOKEN ?? "";
|
|
@@ -173,3 +174,8 @@ async function seoJsonLd(route, locale) {
|
|
|
173
174
|
const data = await fetchJson(`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`);
|
|
174
175
|
return data?.fields?.jsonLd ?? [];
|
|
175
176
|
}
|
|
177
|
+
// Счётчики аналитики (общесайтовые). Читаются в analytics.tsx (<SeoAnalytics/>).
|
|
178
|
+
async function seoAnalytics(route = "/", locale) {
|
|
179
|
+
const data = await fetchJson(`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`);
|
|
180
|
+
return data?.fields?.analytics ?? {};
|
|
181
|
+
}
|
package/dist/sitemap-build.js
CHANGED
|
@@ -35,6 +35,30 @@ function escapeXml(s) {
|
|
|
35
35
|
.replace(/"/g, """)
|
|
36
36
|
.replace(/'/g, "'");
|
|
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.
|
|
3
|
+
"version": "0.1.18",
|
|
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",
|
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
"./redirects": {
|
|
20
20
|
"types": "./dist/redirects.d.ts",
|
|
21
21
|
"default": "./dist/redirects.js"
|
|
22
|
+
},
|
|
23
|
+
"./analytics": {
|
|
24
|
+
"types": "./dist/analytics.d.ts",
|
|
25
|
+
"default": "./dist/analytics.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"dist",
|
|
26
30
|
"client.ts",
|
|
27
31
|
"jsonld.tsx",
|
|
32
|
+
"analytics.tsx",
|
|
28
33
|
"sitemap-xsl.ts",
|
|
29
34
|
"sitemap-build.ts",
|
|
30
35
|
"redirects.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; // убрать изображения
|