@webnumseoagent/next 0.1.6 → 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 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) => {
@@ -122,11 +126,24 @@ async function init() {
122
126
  `${appDir}/sitemap.ts`, `${appDir}/sitemap.js`, `${appDir}/sitemap.xml`,
123
127
  `${appDir}/sitemap.xml/route.ts`, `${appDir}/sitemap.xml/route.js`,
124
128
  ]);
129
+ // Стилизованная карта: индекс + под-карты по разделам + XSL-таблица стилей
130
+ // (человекочитаемый вид в браузере, как у RankMath). Всё собирает платформа.
125
131
  if (hasSitemap) skip("sitemap (у сайта уже есть — не трогаем)");
126
- else await ensureFile(
127
- path.join(root, appDir, `sitemap.${ext}`),
128
- `import { seoSitemap } from "${importClient}";\nexport const revalidate = 300;\nexport default async function sitemap() {\n return seoSitemap();\n}\n`,
129
- );
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
+ }
130
147
 
131
148
  // 3. robots.txt (вариант B — byte-exact: отдаём сырой текст с платформы как есть).
132
149
  // Если у сайта уже есть свой robots — НЕ трогаем (его можно передать платформе позже).
@@ -138,7 +155,7 @@ async function init() {
138
155
  if (hasRobots) skip("robots (у сайта уже есть — не трогаем)");
139
156
  else await ensureFile(
140
157
  path.join(root, appDir, `robots.txt/route.${ext}`),
141
- `import { seoRobotsTxt } from "${importClient}";\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`,
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`,
142
159
  );
143
160
 
144
161
  // 4. revalidate-роут (мгновенное применение правок с платформы)
@@ -174,7 +191,7 @@ async function init() {
174
191
  const rel = path.relative(path.join(root, appDir), p);
175
192
  const { expr } = routeFromFile(rel);
176
193
  const code = await fs.readFile(p, "utf8");
177
- const r = await wrapFile(code, expr, importClient);
194
+ const r = await wrapFile(code, expr, clientFor(rel));
178
195
  if (r.ok) { await fs.writeFile(p, r.code); ok(`${path.relative(root, p)} → обёрнут (route: ${expr})`); wrapped++; }
179
196
  else { manual.push({ p, expr, reason: r.reason }); skip(`${path.relative(root, p)} → вручную (${r.reason})`); }
180
197
  }
@@ -183,7 +200,7 @@ async function init() {
183
200
  log("\n Осталось вручную (замени `return { … }` на):");
184
201
  for (const m of manual) {
185
202
  log(` • ${path.relative(root, m.p)}`);
186
- log(` \x1b[36mimport { seoMeta } from "${importClient}";\n return seoMeta({ route: ${m.expr}, locale, fallback: { /* прежний объект */ } });\x1b[0m`);
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`);
187
204
  }
188
205
  }
189
206
 
@@ -193,7 +210,7 @@ async function init() {
193
210
  const layouts = await findLayouts(path.join(root, appDir));
194
211
  for (const lp of layouts) {
195
212
  const code = await fs.readFile(lp, "utf8");
196
- const r = await mountJsonLd(code, importJsonld);
213
+ const r = await mountJsonLd(code, jsonldFor(path.relative(path.join(root, appDir), lp)));
197
214
  if (r.ok) { await fs.writeFile(lp, r.code); ok(`${path.relative(root, lp)} → смонтирован <SeoJsonLd/>`); jsonLdMounted = true; }
198
215
  else if (r.reason === "already" || r.reason === "existing-jsonld") jsonLdMounted = true;
199
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 }[] }>(
package/dist/client.d.ts CHANGED
@@ -4,6 +4,8 @@ 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>;
9
11
  export declare function seoRobotsTxt(): Promise<string>;
package/dist/client.js CHANGED
@@ -11,7 +11,9 @@
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;
17
19
  exports.seoRobotsTxt = seoRobotsTxt;
@@ -83,6 +85,29 @@ async function seoMeta(opts) {
83
85
  return fallback;
84
86
  return mergeIntoMetadata(fallback, data.fields);
85
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; } });
86
111
  // app/sitemap.ts: export default async function sitemap() { return seoSitemap() }
87
112
  async function seoSitemap() {
88
113
  const data = await fetchJson("/sitemap");
@@ -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.6",
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
+ `;