@webnumseoagent/next 0.1.5 → 0.1.7
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 +33 -13
- package/client.ts +42 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +49 -0
- package/dist/sitemap-xsl.d.ts +1 -0
- package/dist/sitemap-xsl.js +69 -0
- package/package.json +2 -1
- package/sitemap-xsl.ts +66 -0
package/bin/cli.mjs
CHANGED
|
@@ -96,7 +96,7 @@ async function init() {
|
|
|
96
96
|
ok("Пакет @webnumseoagent/next установлен — импорт из пакета.");
|
|
97
97
|
} else {
|
|
98
98
|
const vendor = path.join(root, "seoagent");
|
|
99
|
-
for (const f of ["client.ts", "jsonld.tsx"]) {
|
|
99
|
+
for (const f of ["client.ts", "jsonld.tsx", "sitemap-xsl.ts"]) {
|
|
100
100
|
const dest = path.join(vendor, f);
|
|
101
101
|
if (!(await exists(dest))) {
|
|
102
102
|
await fs.mkdir(vendor, { recursive: true });
|
|
@@ -109,6 +109,10 @@ async function init() {
|
|
|
109
109
|
importJsonld = `${imp}/jsonld`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Импорт клиента/jsonld для файла на нужной глубине (в vendor-режиме путь относительный).
|
|
113
|
+
const clientFor = (file) => (pkgMode ? "@webnumseoagent/next" : `${importPath(appDir, file)}/client`);
|
|
114
|
+
const jsonldFor = (file) => (pkgMode ? "@webnumseoagent/next/jsonld" : `${importPath(appDir, file)}/jsonld`);
|
|
115
|
+
|
|
112
116
|
// Не создаём sitemap/robots, если у сайта они УЖЕ есть (public/, отдельный роут и т.п.) —
|
|
113
117
|
// чтобы не перекрыть существующую карту сайта пустой.
|
|
114
118
|
const anyExists = async (paths) => {
|
|
@@ -119,23 +123,39 @@ async function init() {
|
|
|
119
123
|
// 2. sitemap.ts
|
|
120
124
|
const hasSitemap = await anyExists([
|
|
121
125
|
"public/sitemap.xml", "public/sitemap_index.xml",
|
|
122
|
-
`${appDir}/sitemap.ts`, `${appDir}/sitemap.js`, `${appDir}/sitemap.xml
|
|
126
|
+
`${appDir}/sitemap.ts`, `${appDir}/sitemap.js`, `${appDir}/sitemap.xml`,
|
|
127
|
+
`${appDir}/sitemap.xml/route.ts`, `${appDir}/sitemap.xml/route.js`,
|
|
123
128
|
]);
|
|
129
|
+
// Стилизованная карта: индекс + под-карты по разделам + XSL-таблица стилей
|
|
130
|
+
// (человекочитаемый вид в браузере, как у RankMath). Всё собирает платформа.
|
|
124
131
|
if (hasSitemap) skip("sitemap (у сайта уже есть — не трогаем)");
|
|
125
|
-
else
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
else {
|
|
133
|
+
const tt = ext === "ts";
|
|
134
|
+
await ensureFile(
|
|
135
|
+
path.join(root, appDir, `sitemap.xml/route.${ext}`),
|
|
136
|
+
`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: { "content-type": "application/xml; charset=utf-8", "x-seoagent-sitemap": "1" } });\n}\n`,
|
|
137
|
+
);
|
|
138
|
+
await ensureFile(
|
|
139
|
+
path.join(root, appDir, `sitemap/[name]/route.${ext}`),
|
|
140
|
+
`import { seoSitemapXml } from "${clientFor("sitemap/[name]/route.ts")}";\nexport const revalidate = 300;\nexport async function GET(${tt ? "_req: Request, { params }: { params: { name: string } }" : "_req, { params }"}) {\n return new Response(await seoSitemapXml("/sitemap/" + params.name), { headers: { "content-type": "application/xml; charset=utf-8", "x-seoagent-sitemap": "1" } });\n}\n`,
|
|
141
|
+
);
|
|
142
|
+
await ensureFile(
|
|
143
|
+
path.join(root, appDir, `sitemap.xsl/route.${ext}`),
|
|
144
|
+
`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`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
129
147
|
|
|
130
|
-
// 3. robots
|
|
148
|
+
// 3. robots.txt (вариант B — byte-exact: отдаём сырой текст с платформы как есть).
|
|
149
|
+
// Если у сайта уже есть свой robots — НЕ трогаем (его можно передать платформе позже).
|
|
131
150
|
const hasRobots = await anyExists([
|
|
132
151
|
"public/robots.txt",
|
|
133
|
-
`${appDir}/robots.ts`, `${appDir}/robots.js`, `${appDir}/robots.txt
|
|
152
|
+
`${appDir}/robots.ts`, `${appDir}/robots.js`, `${appDir}/robots.txt`,
|
|
153
|
+
`${appDir}/robots.txt/route.ts`, `${appDir}/robots.txt/route.js`,
|
|
134
154
|
]);
|
|
135
155
|
if (hasRobots) skip("robots (у сайта уже есть — не трогаем)");
|
|
136
156
|
else await ensureFile(
|
|
137
|
-
path.join(root, appDir, `robots.${ext}`),
|
|
138
|
-
`import {
|
|
157
|
+
path.join(root, appDir, `robots.txt/route.${ext}`),
|
|
158
|
+
`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`,
|
|
139
159
|
);
|
|
140
160
|
|
|
141
161
|
// 4. revalidate-роут (мгновенное применение правок с платформы)
|
|
@@ -171,7 +191,7 @@ async function init() {
|
|
|
171
191
|
const rel = path.relative(path.join(root, appDir), p);
|
|
172
192
|
const { expr } = routeFromFile(rel);
|
|
173
193
|
const code = await fs.readFile(p, "utf8");
|
|
174
|
-
const r = await wrapFile(code, expr,
|
|
194
|
+
const r = await wrapFile(code, expr, clientFor(rel));
|
|
175
195
|
if (r.ok) { await fs.writeFile(p, r.code); ok(`${path.relative(root, p)} → обёрнут (route: ${expr})`); wrapped++; }
|
|
176
196
|
else { manual.push({ p, expr, reason: r.reason }); skip(`${path.relative(root, p)} → вручную (${r.reason})`); }
|
|
177
197
|
}
|
|
@@ -180,7 +200,7 @@ async function init() {
|
|
|
180
200
|
log("\n Осталось вручную (замени `return { … }` на):");
|
|
181
201
|
for (const m of manual) {
|
|
182
202
|
log(` • ${path.relative(root, m.p)}`);
|
|
183
|
-
log(` \x1b[36mimport { seoMeta } from "${
|
|
203
|
+
log(` \x1b[36mimport { seoMeta } from "${clientFor(path.relative(path.join(root, appDir), m.p))}";\n return seoMeta({ route: ${m.expr}, locale, fallback: { /* прежний объект */ } });\x1b[0m`);
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
@@ -190,7 +210,7 @@ async function init() {
|
|
|
190
210
|
const layouts = await findLayouts(path.join(root, appDir));
|
|
191
211
|
for (const lp of layouts) {
|
|
192
212
|
const code = await fs.readFile(lp, "utf8");
|
|
193
|
-
const r = await mountJsonLd(code,
|
|
213
|
+
const r = await mountJsonLd(code, jsonldFor(path.relative(path.join(root, appDir), lp)));
|
|
194
214
|
if (r.ok) { await fs.writeFile(lp, r.code); ok(`${path.relative(root, lp)} → смонтирован <SeoJsonLd/>`); jsonLdMounted = true; }
|
|
195
215
|
else if (r.reason === "already" || r.reason === "existing-jsonld") jsonLdMounted = true;
|
|
196
216
|
}
|
package/client.ts
CHANGED
|
@@ -92,6 +92,27 @@ export async function seoMeta(opts: {
|
|
|
92
92
|
return mergeIntoMetadata(fallback, data.fields);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Стилизованная карта: сырой XML с платформы (индекс/urlset со ссылкой на XSL).
|
|
96
|
+
// app/sitemap.xml/route.ts: GET() { return new Response(await seoSitemapXml("/sitemap.xml"), {headers:{"content-type":"application/xml"}}) }
|
|
97
|
+
// app/sitemap/[name]/route.ts: GET(_, {params}) { return new Response(await seoSitemapXml("/sitemap/" + params.name), ...) }
|
|
98
|
+
export async function seoSitemapXml(path: string): Promise<string> {
|
|
99
|
+
const FALLBACK = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>\n`;
|
|
100
|
+
if (!API || !SITE || !TOKEN) return FALLBACK;
|
|
101
|
+
try {
|
|
102
|
+
const r = await fetch(`${API}/api/seo/${SITE}${path}`, {
|
|
103
|
+
headers: { "x-seo-agent-key": TOKEN },
|
|
104
|
+
next: { revalidate: 300, tags: [`seo-${SITE}`] },
|
|
105
|
+
signal: AbortSignal.timeout(5000),
|
|
106
|
+
});
|
|
107
|
+
if (!r.ok) return FALLBACK;
|
|
108
|
+
return await r.text();
|
|
109
|
+
} catch {
|
|
110
|
+
return FALLBACK;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { SITEMAP_XSL } from "./sitemap-xsl";
|
|
115
|
+
|
|
95
116
|
// app/sitemap.ts: export default async function sitemap() { return seoSitemap() }
|
|
96
117
|
export async function seoSitemap(): Promise<MetadataRoute.Sitemap> {
|
|
97
118
|
const data = await fetchJson<{ entries: { url: string; alternates: Record<string, string>; lastModified?: string }[] }>(
|
|
@@ -112,6 +133,27 @@ export async function seoRobots(): Promise<MetadataRoute.Robots> {
|
|
|
112
133
|
return data ?? { rules: [{ userAgent: "*", allow: "/" }] };
|
|
113
134
|
}
|
|
114
135
|
|
|
136
|
+
// Сырой robots.txt (byte-exact, вариант B). Используется в app/robots.txt/route.ts:
|
|
137
|
+
// export async function GET() {
|
|
138
|
+
// return new Response(await seoRobotsTxt(), { headers: { "content-type": "text/plain" } });
|
|
139
|
+
// }
|
|
140
|
+
// Fail-safe: при недоступности платформы отдаём разрешающий robots (никогда не Disallow: /).
|
|
141
|
+
export async function seoRobotsTxt(): Promise<string> {
|
|
142
|
+
const FALLBACK = "User-agent: *\nAllow: /\n";
|
|
143
|
+
if (!API || !SITE || !TOKEN) return FALLBACK;
|
|
144
|
+
try {
|
|
145
|
+
const r = await fetch(`${API}/api/seo/${SITE}/robots.txt`, {
|
|
146
|
+
headers: { "x-seo-agent-key": TOKEN },
|
|
147
|
+
next: { revalidate: 300, tags: [`seo-${SITE}`] },
|
|
148
|
+
signal: AbortSignal.timeout(5000),
|
|
149
|
+
});
|
|
150
|
+
if (!r.ok) return FALLBACK;
|
|
151
|
+
return await r.text();
|
|
152
|
+
} catch {
|
|
153
|
+
return FALLBACK;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
115
157
|
// Для JSON-LD берём массив узлов конфига данного маршрута/локали (см. jsonld.tsx).
|
|
116
158
|
export async function seoJsonLd(route: string, locale?: string): Promise<unknown[]> {
|
|
117
159
|
const data = await fetchJson<{ fields: SeoFields }>(
|
package/dist/client.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare function seoMeta(opts: {
|
|
|
4
4
|
locale?: string;
|
|
5
5
|
fallback?: Metadata;
|
|
6
6
|
}): Promise<Metadata>;
|
|
7
|
+
export declare function seoSitemapXml(path: string): Promise<string>;
|
|
8
|
+
export { SITEMAP_XSL } from "./sitemap-xsl";
|
|
7
9
|
export declare function seoSitemap(): Promise<MetadataRoute.Sitemap>;
|
|
8
10
|
export declare function seoRobots(): Promise<MetadataRoute.Robots>;
|
|
11
|
+
export declare function seoRobotsTxt(): Promise<string>;
|
|
9
12
|
export declare function seoJsonLd(route: string, locale?: string): Promise<unknown[]>;
|
package/dist/client.js
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
// Все хелперы FAIL-SAFE: при ошибке/таймауте возвращают переданный fallback
|
|
12
12
|
// (скомпилированные дефолты сайта) — сборка/рендер НИКОГДА не падает и НЕ уходит в noindex.
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SITEMAP_XSL = void 0;
|
|
14
15
|
exports.seoMeta = seoMeta;
|
|
16
|
+
exports.seoSitemapXml = seoSitemapXml;
|
|
15
17
|
exports.seoSitemap = seoSitemap;
|
|
16
18
|
exports.seoRobots = seoRobots;
|
|
19
|
+
exports.seoRobotsTxt = seoRobotsTxt;
|
|
17
20
|
exports.seoJsonLd = seoJsonLd;
|
|
18
21
|
const API = process.env.SEOAGENT_API_BASE ?? "";
|
|
19
22
|
const SITE = process.env.SEOAGENT_SITE_ID ?? "";
|
|
@@ -82,6 +85,29 @@ async function seoMeta(opts) {
|
|
|
82
85
|
return fallback;
|
|
83
86
|
return mergeIntoMetadata(fallback, data.fields);
|
|
84
87
|
}
|
|
88
|
+
// Стилизованная карта: сырой XML с платформы (индекс/urlset со ссылкой на XSL).
|
|
89
|
+
// app/sitemap.xml/route.ts: GET() { return new Response(await seoSitemapXml("/sitemap.xml"), {headers:{"content-type":"application/xml"}}) }
|
|
90
|
+
// app/sitemap/[name]/route.ts: GET(_, {params}) { return new Response(await seoSitemapXml("/sitemap/" + params.name), ...) }
|
|
91
|
+
async function seoSitemapXml(path) {
|
|
92
|
+
const FALLBACK = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>\n`;
|
|
93
|
+
if (!API || !SITE || !TOKEN)
|
|
94
|
+
return FALLBACK;
|
|
95
|
+
try {
|
|
96
|
+
const r = await fetch(`${API}/api/seo/${SITE}${path}`, {
|
|
97
|
+
headers: { "x-seo-agent-key": TOKEN },
|
|
98
|
+
next: { revalidate: 300, tags: [`seo-${SITE}`] },
|
|
99
|
+
signal: AbortSignal.timeout(5000),
|
|
100
|
+
});
|
|
101
|
+
if (!r.ok)
|
|
102
|
+
return FALLBACK;
|
|
103
|
+
return await r.text();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return FALLBACK;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
var sitemap_xsl_1 = require("./sitemap-xsl");
|
|
110
|
+
Object.defineProperty(exports, "SITEMAP_XSL", { enumerable: true, get: function () { return sitemap_xsl_1.SITEMAP_XSL; } });
|
|
85
111
|
// app/sitemap.ts: export default async function sitemap() { return seoSitemap() }
|
|
86
112
|
async function seoSitemap() {
|
|
87
113
|
const data = await fetchJson("/sitemap");
|
|
@@ -99,6 +125,29 @@ async function seoRobots() {
|
|
|
99
125
|
// Fail-safe: при недоступности отдаём разрешающий robots (никогда не Disallow: /).
|
|
100
126
|
return data ?? { rules: [{ userAgent: "*", allow: "/" }] };
|
|
101
127
|
}
|
|
128
|
+
// Сырой robots.txt (byte-exact, вариант B). Используется в app/robots.txt/route.ts:
|
|
129
|
+
// export async function GET() {
|
|
130
|
+
// return new Response(await seoRobotsTxt(), { headers: { "content-type": "text/plain" } });
|
|
131
|
+
// }
|
|
132
|
+
// Fail-safe: при недоступности платформы отдаём разрешающий robots (никогда не Disallow: /).
|
|
133
|
+
async function seoRobotsTxt() {
|
|
134
|
+
const FALLBACK = "User-agent: *\nAllow: /\n";
|
|
135
|
+
if (!API || !SITE || !TOKEN)
|
|
136
|
+
return FALLBACK;
|
|
137
|
+
try {
|
|
138
|
+
const r = await fetch(`${API}/api/seo/${SITE}/robots.txt`, {
|
|
139
|
+
headers: { "x-seo-agent-key": TOKEN },
|
|
140
|
+
next: { revalidate: 300, tags: [`seo-${SITE}`] },
|
|
141
|
+
signal: AbortSignal.timeout(5000),
|
|
142
|
+
});
|
|
143
|
+
if (!r.ok)
|
|
144
|
+
return FALLBACK;
|
|
145
|
+
return await r.text();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return FALLBACK;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
102
151
|
// Для JSON-LD берём массив узлов конфига данного маршрута/локали (см. jsonld.tsx).
|
|
103
152
|
async function seoJsonLd(route, locale) {
|
|
104
153
|
const data = await fetchJson(`/config?route=${encodeURIComponent(route)}&locale=${encodeURIComponent(locale ?? "")}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SITEMAP_XSL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xsl:stylesheet version=\"1.0\"\n xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n xmlns:s=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n<xsl:output method=\"html\" encoding=\"UTF-8\" indent=\"yes\"/>\n<xsl:template match=\"/\">\n<html lang=\"ru\">\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>XML-\u043A\u0430\u0440\u0442\u0430 \u0441\u0430\u0439\u0442\u0430</title>\n<style>\n *{box-sizing:border-box}\n body{font-family:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;color:#1e293b;margin:0;background:#f8fafc}\n .head{background:#2563eb;color:#fff;padding:28px 40px}\n .head h1{margin:0 0 8px;font-size:26px;font-weight:600}\n .head p{margin:0;font-size:14px;opacity:.92;max-width:920px;line-height:1.55}\n .head a{color:#fff}\n .wrap{padding:24px 40px}\n .count{color:#475569;font-size:14px;margin:0 0 14px}\n table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}\n th{background:#eff2f7;color:#334155;text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.03em;padding:12px 16px}\n td{padding:11px 16px;font-size:14px;border-top:1px solid #f1f5f9}\n tr:hover td{background:#f8fafc}\n td a{color:#2563eb;text-decoration:none;word-break:break-all}\n td a:hover{text-decoration:underline}\n .n{width:80px;color:#64748b;text-align:right}\n</style>\n</head>\n<body>\n<div class=\"head\">\n <h1>XML-\u043A\u0430\u0440\u0442\u0430 \u0441\u0430\u0439\u0442\u0430</h1>\n <p>\u0421\u043F\u0438\u0441\u043E\u043A \u0441\u0442\u0440\u0430\u043D\u0438\u0446 \u0434\u043B\u044F \u043F\u043E\u0438\u0441\u043A\u043E\u0432\u044B\u0445 \u0441\u0438\u0441\u0442\u0435\u043C. \u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u0439 <b>SEO Agent</b>. \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u0435\u0435 \u043E\u0431 <a href=\"https://www.sitemaps.org/\">XML-\u043A\u0430\u0440\u0442\u0430\u0445 \u0441\u0430\u0439\u0442\u0430</a>.</p>\n</div>\n<div class=\"wrap\">\n<xsl:choose>\n <xsl:when test=\"s:sitemapindex\">\n <p class=\"count\">\u0418\u043D\u0434\u0435\u043A\u0441\u043D\u044B\u0439 \u0444\u0430\u0439\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 <b><xsl:value-of select=\"count(s:sitemapindex/s:sitemap)\"/></b> \u043A\u0430\u0440\u0442(\u044B) \u0441\u0430\u0439\u0442\u0430.</p>\n <table>\n <tr><th>\u041A\u0430\u0440\u0442\u0430 \u0441\u0430\u0439\u0442\u0430</th></tr>\n <xsl:for-each select=\"s:sitemapindex/s:sitemap\">\n <tr><td><a href=\"{s:loc}\"><xsl:value-of select=\"s:loc\"/></a></td></tr>\n </xsl:for-each>\n </table>\n </xsl:when>\n <xsl:otherwise>\n <p class=\"count\">\u042D\u0442\u0430 \u043A\u0430\u0440\u0442\u0430 \u0441\u0430\u0439\u0442\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 <b><xsl:value-of select=\"count(s:urlset/s:url)\"/></b> URL.</p>\n <table>\n <tr><th class=\"n\">#</th><th>URL</th></tr>\n <xsl:for-each select=\"s:urlset/s:url\">\n <tr>\n <td class=\"n\"><xsl:value-of select=\"position()\"/></td>\n <td><a href=\"{s:loc}\"><xsl:value-of select=\"s:loc\"/></a></td>\n </tr>\n </xsl:for-each>\n </table>\n </xsl:otherwise>\n</xsl:choose>\n</div>\n</body>\n</html>\n</xsl:template>\n</xsl:stylesheet>\n";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SITEMAP_XSL = void 0;
|
|
4
|
+
// XSL-таблица стилей для человекочитаемого вида XML-карты в браузере (как у RankMath).
|
|
5
|
+
// Отдаётся адаптером на домене сайта по /sitemap.xsl; сам XML на неё ссылается.
|
|
6
|
+
exports.SITEMAP_XSL = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<xsl:stylesheet version="1.0"
|
|
8
|
+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
9
|
+
xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
10
|
+
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
|
|
11
|
+
<xsl:template match="/">
|
|
12
|
+
<html lang="ru">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8"/>
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
16
|
+
<title>XML-карта сайта</title>
|
|
17
|
+
<style>
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
body{font-family:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;color:#1e293b;margin:0;background:#f8fafc}
|
|
20
|
+
.head{background:#2563eb;color:#fff;padding:28px 40px}
|
|
21
|
+
.head h1{margin:0 0 8px;font-size:26px;font-weight:600}
|
|
22
|
+
.head p{margin:0;font-size:14px;opacity:.92;max-width:920px;line-height:1.55}
|
|
23
|
+
.head a{color:#fff}
|
|
24
|
+
.wrap{padding:24px 40px}
|
|
25
|
+
.count{color:#475569;font-size:14px;margin:0 0 14px}
|
|
26
|
+
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
|
|
27
|
+
th{background:#eff2f7;color:#334155;text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.03em;padding:12px 16px}
|
|
28
|
+
td{padding:11px 16px;font-size:14px;border-top:1px solid #f1f5f9}
|
|
29
|
+
tr:hover td{background:#f8fafc}
|
|
30
|
+
td a{color:#2563eb;text-decoration:none;word-break:break-all}
|
|
31
|
+
td a:hover{text-decoration:underline}
|
|
32
|
+
.n{width:80px;color:#64748b;text-align:right}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div class="head">
|
|
37
|
+
<h1>XML-карта сайта</h1>
|
|
38
|
+
<p>Список страниц для поисковых систем. Сгенерировано платформой <b>SEO Agent</b>. Подробнее об <a href="https://www.sitemaps.org/">XML-картах сайта</a>.</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="wrap">
|
|
41
|
+
<xsl:choose>
|
|
42
|
+
<xsl:when test="s:sitemapindex">
|
|
43
|
+
<p class="count">Индексный файл содержит <b><xsl:value-of select="count(s:sitemapindex/s:sitemap)"/></b> карт(ы) сайта.</p>
|
|
44
|
+
<table>
|
|
45
|
+
<tr><th>Карта сайта</th></tr>
|
|
46
|
+
<xsl:for-each select="s:sitemapindex/s:sitemap">
|
|
47
|
+
<tr><td><a href="{s:loc}"><xsl:value-of select="s:loc"/></a></td></tr>
|
|
48
|
+
</xsl:for-each>
|
|
49
|
+
</table>
|
|
50
|
+
</xsl:when>
|
|
51
|
+
<xsl:otherwise>
|
|
52
|
+
<p class="count">Эта карта сайта содержит <b><xsl:value-of select="count(s:urlset/s:url)"/></b> URL.</p>
|
|
53
|
+
<table>
|
|
54
|
+
<tr><th class="n">#</th><th>URL</th></tr>
|
|
55
|
+
<xsl:for-each select="s:urlset/s:url">
|
|
56
|
+
<tr>
|
|
57
|
+
<td class="n"><xsl:value-of select="position()"/></td>
|
|
58
|
+
<td><a href="{s:loc}"><xsl:value-of select="s:loc"/></a></td>
|
|
59
|
+
</tr>
|
|
60
|
+
</xsl:for-each>
|
|
61
|
+
</table>
|
|
62
|
+
</xsl:otherwise>
|
|
63
|
+
</xsl:choose>
|
|
64
|
+
</div>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
67
|
+
</xsl:template>
|
|
68
|
+
</xsl:stylesheet>
|
|
69
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webnumseoagent/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dist",
|
|
22
22
|
"client.ts",
|
|
23
23
|
"jsonld.tsx",
|
|
24
|
+
"sitemap-xsl.ts",
|
|
24
25
|
"bin",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
package/sitemap-xsl.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// XSL-таблица стилей для человекочитаемого вида XML-карты в браузере (как у RankMath).
|
|
2
|
+
// Отдаётся адаптером на домене сайта по /sitemap.xsl; сам XML на неё ссылается.
|
|
3
|
+
export const SITEMAP_XSL = `<?xml version="1.0" encoding="UTF-8"?>
|
|
4
|
+
<xsl:stylesheet version="1.0"
|
|
5
|
+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
6
|
+
xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
7
|
+
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
|
|
8
|
+
<xsl:template match="/">
|
|
9
|
+
<html lang="ru">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8"/>
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
13
|
+
<title>XML-карта сайта</title>
|
|
14
|
+
<style>
|
|
15
|
+
*{box-sizing:border-box}
|
|
16
|
+
body{font-family:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;color:#1e293b;margin:0;background:#f8fafc}
|
|
17
|
+
.head{background:#2563eb;color:#fff;padding:28px 40px}
|
|
18
|
+
.head h1{margin:0 0 8px;font-size:26px;font-weight:600}
|
|
19
|
+
.head p{margin:0;font-size:14px;opacity:.92;max-width:920px;line-height:1.55}
|
|
20
|
+
.head a{color:#fff}
|
|
21
|
+
.wrap{padding:24px 40px}
|
|
22
|
+
.count{color:#475569;font-size:14px;margin:0 0 14px}
|
|
23
|
+
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
|
|
24
|
+
th{background:#eff2f7;color:#334155;text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.03em;padding:12px 16px}
|
|
25
|
+
td{padding:11px 16px;font-size:14px;border-top:1px solid #f1f5f9}
|
|
26
|
+
tr:hover td{background:#f8fafc}
|
|
27
|
+
td a{color:#2563eb;text-decoration:none;word-break:break-all}
|
|
28
|
+
td a:hover{text-decoration:underline}
|
|
29
|
+
.n{width:80px;color:#64748b;text-align:right}
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<div class="head">
|
|
34
|
+
<h1>XML-карта сайта</h1>
|
|
35
|
+
<p>Список страниц для поисковых систем. Сгенерировано платформой <b>SEO Agent</b>. Подробнее об <a href="https://www.sitemaps.org/">XML-картах сайта</a>.</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="wrap">
|
|
38
|
+
<xsl:choose>
|
|
39
|
+
<xsl:when test="s:sitemapindex">
|
|
40
|
+
<p class="count">Индексный файл содержит <b><xsl:value-of select="count(s:sitemapindex/s:sitemap)"/></b> карт(ы) сайта.</p>
|
|
41
|
+
<table>
|
|
42
|
+
<tr><th>Карта сайта</th></tr>
|
|
43
|
+
<xsl:for-each select="s:sitemapindex/s:sitemap">
|
|
44
|
+
<tr><td><a href="{s:loc}"><xsl:value-of select="s:loc"/></a></td></tr>
|
|
45
|
+
</xsl:for-each>
|
|
46
|
+
</table>
|
|
47
|
+
</xsl:when>
|
|
48
|
+
<xsl:otherwise>
|
|
49
|
+
<p class="count">Эта карта сайта содержит <b><xsl:value-of select="count(s:urlset/s:url)"/></b> URL.</p>
|
|
50
|
+
<table>
|
|
51
|
+
<tr><th class="n">#</th><th>URL</th></tr>
|
|
52
|
+
<xsl:for-each select="s:urlset/s:url">
|
|
53
|
+
<tr>
|
|
54
|
+
<td class="n"><xsl:value-of select="position()"/></td>
|
|
55
|
+
<td><a href="{s:loc}"><xsl:value-of select="s:loc"/></a></td>
|
|
56
|
+
</tr>
|
|
57
|
+
</xsl:for-each>
|
|
58
|
+
</table>
|
|
59
|
+
</xsl:otherwise>
|
|
60
|
+
</xsl:choose>
|
|
61
|
+
</div>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
</xsl:template>
|
|
65
|
+
</xsl:stylesheet>
|
|
66
|
+
`;
|