@prenta/core 7.6.6 → 7.7.1
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/CHANGELOG.md +12 -0
- package/dist/api/routes/seo/handlers/analysis.d.ts +4 -0
- package/dist/api/routes/seo/handlers/analysis.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/analysis.js +131 -0
- package/dist/api/routes/seo/handlers/analysis.js.map +1 -0
- package/dist/api/routes/seo/handlers/audits.d.ts +5 -0
- package/dist/api/routes/seo/handlers/audits.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/audits.js +121 -0
- package/dist/api/routes/seo/handlers/audits.js.map +1 -0
- package/dist/api/routes/seo/handlers/content.d.ts +4 -0
- package/dist/api/routes/seo/handlers/content.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/content.js +330 -0
- package/dist/api/routes/seo/handlers/content.js.map +1 -0
- package/dist/api/routes/seo/handlers/issue-helpers.d.ts +37 -0
- package/dist/api/routes/seo/handlers/issue-helpers.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/issue-helpers.js +72 -0
- package/dist/api/routes/seo/handlers/issue-helpers.js.map +1 -0
- package/dist/api/routes/seo/handlers/issues-briefs.d.ts +7 -0
- package/dist/api/routes/seo/handlers/issues-briefs.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/issues-briefs.js +341 -0
- package/dist/api/routes/seo/handlers/issues-briefs.js.map +1 -0
- package/dist/api/routes/seo/handlers/issues-fixes.d.ts +4 -0
- package/dist/api/routes/seo/handlers/issues-fixes.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/issues-fixes.js +233 -0
- package/dist/api/routes/seo/handlers/issues-fixes.js.map +1 -0
- package/dist/api/routes/seo/handlers/issues-list.d.ts +7 -0
- package/dist/api/routes/seo/handlers/issues-list.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/issues-list.js +191 -0
- package/dist/api/routes/seo/handlers/issues-list.js.map +1 -0
- package/dist/api/routes/seo/handlers/links.d.ts +6 -0
- package/dist/api/routes/seo/handlers/links.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/links.js +383 -0
- package/dist/api/routes/seo/handlers/links.js.map +1 -0
- package/dist/api/routes/seo/handlers/overview.d.ts +4 -0
- package/dist/api/routes/seo/handlers/overview.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/overview.js +348 -0
- package/dist/api/routes/seo/handlers/overview.js.map +1 -0
- package/dist/api/routes/seo/handlers/pages.d.ts +2 -0
- package/dist/api/routes/seo/handlers/pages.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/pages.js +38 -0
- package/dist/api/routes/seo/handlers/pages.js.map +1 -0
- package/dist/api/routes/seo/handlers/proposals.d.ts +4 -0
- package/dist/api/routes/seo/handlers/proposals.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/proposals.js +93 -0
- package/dist/api/routes/seo/handlers/proposals.js.map +1 -0
- package/dist/api/routes/seo/handlers/public-files.d.ts +6 -0
- package/dist/api/routes/seo/handlers/public-files.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/public-files.js +328 -0
- package/dist/api/routes/seo/handlers/public-files.js.map +1 -0
- package/dist/api/routes/seo/handlers/shared.d.ts +24 -0
- package/dist/api/routes/seo/handlers/shared.d.ts.map +1 -0
- package/dist/api/routes/seo/handlers/shared.js +67 -0
- package/dist/api/routes/seo/handlers/shared.js.map +1 -0
- package/dist/api/routes/seo.d.ts +5 -5
- package/dist/api/routes/seo.d.ts.map +1 -1
- package/dist/api/routes/seo.js +56 -2610
- package/dist/api/routes/seo.js.map +1 -1
- package/dist/cache/http.d.ts +5 -0
- package/dist/cache/http.d.ts.map +1 -1
- package/dist/cache/http.js +6 -0
- package/dist/cache/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next-site.d.ts.map +1 -1
- package/dist/next-site.js +6 -1
- package/dist/next-site.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { getPrentaConfig } from '../../../../config/runtime.js';
|
|
2
|
+
import { recordAiCrawlerFromRequest, recordAiCrawlerHit } from '../../../../seo/ai-crawler-log.js';
|
|
3
|
+
import { detectAiBot } from '../../../../seo/ai-bots.js';
|
|
4
|
+
import { json, errorResponse, internalError, hasModel, requireAuth, } from '../../../route-helpers.js';
|
|
5
|
+
import { db } from './shared.js';
|
|
6
|
+
export async function getLlmsTxt(request) {
|
|
7
|
+
try {
|
|
8
|
+
// Public surface: must exclude soft-deleted docs and internal/system
|
|
9
|
+
// collections (e.g. `__seo_config__`). Over-fetch then filter so the
|
|
10
|
+
// 50-page cap still lands on real content after exclusion.
|
|
11
|
+
const { isInternalCollection } = await import('../../../../seo/audit-runner.js');
|
|
12
|
+
const { siteUrlFromRequest, loadEffectiveConfig } = await import('../../seo-public.js');
|
|
13
|
+
// Layer DB overrides (Settings → SEO) so an admin-edited siteName /
|
|
14
|
+
// defaultMetaDescription flows into the llms heading + summary too, exactly
|
|
15
|
+
// as sitemap.xml, robots.txt, /og.png and the feeds already do.
|
|
16
|
+
const cfg = (await loadEffectiveConfig()) ?? getPrentaConfig();
|
|
17
|
+
const llmsCfg = cfg?.seo?.llms;
|
|
18
|
+
const base = siteUrlFromRequest(request, cfg);
|
|
19
|
+
// Operators can disable the file entirely (it's optionality, not a
|
|
20
|
+
// guaranteed traffic channel).
|
|
21
|
+
if (llmsCfg?.disabled) {
|
|
22
|
+
return new Response('Not found', { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
const maxEntries = llmsCfg?.maxEntries ?? 50;
|
|
25
|
+
const maxPerSection = llmsCfg?.maxPerSection ?? 25;
|
|
26
|
+
// Restrict AUTO selection to the configured high-value collections when
|
|
27
|
+
// set, so blog archives don't flood the curated guide.
|
|
28
|
+
const eligible = Array.isArray(llmsCfg?.eligibleCollections) && llmsCfg.eligibleCollections.length > 0
|
|
29
|
+
? new Set(llmsCfg.eligibleCollections)
|
|
30
|
+
: null;
|
|
31
|
+
const rawDocs = await db().document.findMany({
|
|
32
|
+
where: { status: 'PUBLISHED', deletedAt: null },
|
|
33
|
+
select: { title: true, slug: true, collection: true, updatedAt: true, data: true },
|
|
34
|
+
orderBy: { updatedAt: 'desc' },
|
|
35
|
+
take: 500,
|
|
36
|
+
});
|
|
37
|
+
// Per-doc curation + privacy boundary (noindex exclusion) live in the
|
|
38
|
+
// shared selector so `/llms.txt` and `/llms-full.txt` can never diverge.
|
|
39
|
+
const { selectLlmsDocs, toLlmsPage } = await import('../../../../seo/llms-select.js');
|
|
40
|
+
const selected = selectLlmsDocs(rawDocs, { isInternalCollection, eligible });
|
|
41
|
+
const { generateLlmsTxt } = await import('../../../../seo/llms-txt.js');
|
|
42
|
+
const pages = selected.map((d) => toLlmsPage(d, {
|
|
43
|
+
base,
|
|
44
|
+
urlPrefix: cfg?.collections?.[d.collection]?.urlPrefix,
|
|
45
|
+
trailingSlash: cfg?.seo?.trailingSlash,
|
|
46
|
+
}));
|
|
47
|
+
const txt = generateLlmsTxt({
|
|
48
|
+
siteName: cfg?.seo?.siteName || 'Prenta CMS',
|
|
49
|
+
siteUrl: base,
|
|
50
|
+
siteDescription: llmsCfg?.summary || cfg?.seo?.defaultMetaDescription || 'Content managed by Prenta CMS',
|
|
51
|
+
intro: llmsCfg?.intro,
|
|
52
|
+
maxPages: maxEntries,
|
|
53
|
+
maxPerSection,
|
|
54
|
+
}, pages);
|
|
55
|
+
// Opt-in honeypot: append a unique trap link. No human reads llms.txt, so
|
|
56
|
+
// a fetch of this URL is an automated reader — recorded under the
|
|
57
|
+
// "honeypot" surface in the AI-crawler panel.
|
|
58
|
+
let body = txt;
|
|
59
|
+
if (llmsCfg?.honeypot) {
|
|
60
|
+
const trapUrl = `${base.replace(/\/$/, '')}/api/cms/ai-crawlers/trap`;
|
|
61
|
+
body += `\n## Reference\n\n- [Complete content index](${trapUrl}): Full machine-readable content map.\n`;
|
|
62
|
+
}
|
|
63
|
+
// Measurement: if a known AI crawler fetched the index, count it.
|
|
64
|
+
await recordAiCrawlerFromRequest(db(), request, 'llms');
|
|
65
|
+
return new Response(body, {
|
|
66
|
+
status: 200,
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
69
|
+
// The file itself is a machine-facing index, not a page to rank.
|
|
70
|
+
'X-Robots-Tag': 'noindex',
|
|
71
|
+
// Same CDN TTL as robots.txt — the index is cheap to regenerate
|
|
72
|
+
// after publish but should not be fetched on every agent request.
|
|
73
|
+
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
return internalError(err, 'llms.txt');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// `/llms-full.txt` — same curated selection as `/llms.txt`, but inlines each
|
|
82
|
+
// page's content rendered to clean Markdown so an agent can read the actual
|
|
83
|
+
// body without fetching every URL. Opt-in (these files get large) and bounded
|
|
84
|
+
// by a byte budget. noindex + never in the sitemap; HTML stays canonical.
|
|
85
|
+
export async function getLlmsFullTxt(request) {
|
|
86
|
+
try {
|
|
87
|
+
const { isInternalCollection } = await import('../../../../seo/audit-runner.js');
|
|
88
|
+
const { siteUrlFromRequest, loadEffectiveConfig } = await import('../../seo-public.js');
|
|
89
|
+
// Honor admin overrides (siteName / summary / defaultMetaDescription) the
|
|
90
|
+
// same way `/llms.txt` and the other public SEO surfaces do.
|
|
91
|
+
const cfg = (await loadEffectiveConfig()) ?? getPrentaConfig();
|
|
92
|
+
const llmsCfg = cfg?.seo?.llms;
|
|
93
|
+
const base = siteUrlFromRequest(request, cfg);
|
|
94
|
+
// Gated independently from `/llms.txt`: must be both enabled overall and
|
|
95
|
+
// have the full variant turned on.
|
|
96
|
+
if (llmsCfg?.disabled || !llmsCfg?.fullEnabled) {
|
|
97
|
+
return new Response('Not found', { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
const maxEntries = llmsCfg?.maxEntries ?? 50;
|
|
100
|
+
const maxBytes = llmsCfg?.maxFullBytes ?? 2_000_000;
|
|
101
|
+
const eligible = Array.isArray(llmsCfg?.eligibleCollections) && llmsCfg.eligibleCollections.length > 0
|
|
102
|
+
? new Set(llmsCfg.eligibleCollections)
|
|
103
|
+
: null;
|
|
104
|
+
const rawDocs = await db().document.findMany({
|
|
105
|
+
where: { status: 'PUBLISHED', deletedAt: null },
|
|
106
|
+
select: {
|
|
107
|
+
title: true,
|
|
108
|
+
slug: true,
|
|
109
|
+
collection: true,
|
|
110
|
+
updatedAt: true,
|
|
111
|
+
data: true,
|
|
112
|
+
plainText: true,
|
|
113
|
+
},
|
|
114
|
+
orderBy: { updatedAt: 'desc' },
|
|
115
|
+
take: 500,
|
|
116
|
+
});
|
|
117
|
+
const { selectLlmsDocs, toLlmsPage } = await import('../../../../seo/llms-select.js');
|
|
118
|
+
const { documentToMarkdown } = await import('../../../../seo/content-markdown.js');
|
|
119
|
+
const selected = selectLlmsDocs(rawDocs, { isInternalCollection, eligible }).slice(0, maxEntries);
|
|
120
|
+
const header = [
|
|
121
|
+
`# ${cfg?.seo?.siteName || 'Prenta CMS'}`,
|
|
122
|
+
'',
|
|
123
|
+
`> ${llmsCfg?.summary || cfg?.seo?.defaultMetaDescription || 'Full content export for AI agents.'}`,
|
|
124
|
+
'',
|
|
125
|
+
].join('\n');
|
|
126
|
+
const chunks = [header];
|
|
127
|
+
let bytes = Buffer.byteLength(header, 'utf8');
|
|
128
|
+
let truncated = false;
|
|
129
|
+
for (const d of selected) {
|
|
130
|
+
const page = toLlmsPage(d, {
|
|
131
|
+
base,
|
|
132
|
+
urlPrefix: cfg?.collections?.[d.collection]?.urlPrefix,
|
|
133
|
+
trailingSlash: cfg?.seo?.trailingSlash,
|
|
134
|
+
});
|
|
135
|
+
const md = documentToMarkdown({
|
|
136
|
+
title: d.title,
|
|
137
|
+
data: d.data,
|
|
138
|
+
plainText: d.plainText,
|
|
139
|
+
}, { baseUrl: base });
|
|
140
|
+
// Each entry is delimited by a horizontal rule and carries its canonical
|
|
141
|
+
// URL so the inlined content stays traceable back to the live page.
|
|
142
|
+
const block = `\n---\n\nURL: ${page.url}\n\n${md}\n`;
|
|
143
|
+
const blockBytes = Buffer.byteLength(block, 'utf8');
|
|
144
|
+
if (bytes + blockBytes > maxBytes) {
|
|
145
|
+
truncated = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
chunks.push(block);
|
|
149
|
+
bytes += blockBytes;
|
|
150
|
+
}
|
|
151
|
+
if (truncated) {
|
|
152
|
+
chunks.push('\n---\n\n> Output truncated at the configured size limit.\n');
|
|
153
|
+
}
|
|
154
|
+
await recordAiCrawlerFromRequest(db(), request, 'llms-full');
|
|
155
|
+
return new Response(chunks.join(''), {
|
|
156
|
+
status: 200,
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
159
|
+
'X-Robots-Tag': 'noindex',
|
|
160
|
+
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return internalError(err, 'llms-full.txt');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Per-page Markdown: `GET /md?path=/about` returns a clean Markdown rendering
|
|
169
|
+
// of a single published page so an agent can fetch the content, not just the
|
|
170
|
+
// link. Fresh installs expose this at clean `/{path}.md` URLs via a scaffolded
|
|
171
|
+
// rewrite. CRITICAL: always noindex, never in the sitemap — the HTML page
|
|
172
|
+
// stays canonical (per-page `.md` is the most common duplicate-content trap).
|
|
173
|
+
export async function getMarkdownForPath(request) {
|
|
174
|
+
try {
|
|
175
|
+
const cfg = getPrentaConfig();
|
|
176
|
+
const url = new URL(request.url);
|
|
177
|
+
const pathParam = url.searchParams.get('path');
|
|
178
|
+
if (!pathParam) {
|
|
179
|
+
return errorResponse('Missing required "path" query parameter', 400);
|
|
180
|
+
}
|
|
181
|
+
// Part of the AI/agent layer — honor the same master switch as llms.txt.
|
|
182
|
+
if (cfg?.seo?.llms?.disabled) {
|
|
183
|
+
return new Response('Not found', { status: 404 });
|
|
184
|
+
}
|
|
185
|
+
const { matchPathToCollection } = await import('../../../../seo/resolve-path.js');
|
|
186
|
+
const match = matchPathToCollection(cfg, pathParam);
|
|
187
|
+
if (!match) {
|
|
188
|
+
return new Response('Not found', { status: 404 });
|
|
189
|
+
}
|
|
190
|
+
const slugFilter = match.isRoot
|
|
191
|
+
? {
|
|
192
|
+
OR: [
|
|
193
|
+
{ data: { path: ['slug'], equals: 'home' } },
|
|
194
|
+
{ data: { path: ['slug'], equals: 'index' } },
|
|
195
|
+
{ slug: 'home' },
|
|
196
|
+
{ slug: 'index' },
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
: {
|
|
200
|
+
OR: [{ data: { path: ['slug'], equals: match.slug } }, { slug: match.slug }],
|
|
201
|
+
};
|
|
202
|
+
const doc = await db().document.findFirst({
|
|
203
|
+
where: {
|
|
204
|
+
collection: match.collection,
|
|
205
|
+
deletedAt: null,
|
|
206
|
+
status: 'PUBLISHED',
|
|
207
|
+
...slugFilter,
|
|
208
|
+
},
|
|
209
|
+
select: { title: true, data: true, plainText: true },
|
|
210
|
+
});
|
|
211
|
+
if (!doc) {
|
|
212
|
+
return new Response('Not found', { status: 404 });
|
|
213
|
+
}
|
|
214
|
+
const data = doc.data || {};
|
|
215
|
+
// The selection filter is the privacy boundary: noindexed or explicitly
|
|
216
|
+
// excluded pages must not be fetchable as Markdown either.
|
|
217
|
+
const { isLlmsNoindex, readLlmsInclude } = await import('../../../../seo/llms-select.js');
|
|
218
|
+
if (isLlmsNoindex(data) || readLlmsInclude(data) === 'NEVER') {
|
|
219
|
+
return new Response('Not found', { status: 404 });
|
|
220
|
+
}
|
|
221
|
+
const { documentToMarkdown } = await import('../../../../seo/content-markdown.js');
|
|
222
|
+
const { siteUrlFromRequest } = await import('../../seo-public.js');
|
|
223
|
+
const base = siteUrlFromRequest(request, cfg);
|
|
224
|
+
const md = documentToMarkdown({ title: doc.title, data, plainText: doc.plainText }, { baseUrl: base });
|
|
225
|
+
await recordAiCrawlerFromRequest(db(), request, 'md');
|
|
226
|
+
return new Response(md, {
|
|
227
|
+
status: 200,
|
|
228
|
+
headers: {
|
|
229
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
230
|
+
'X-Robots-Tag': 'noindex',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
return internalError(err, 'md');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Honeypot: a unique trap URL appended to llms.txt only when
|
|
239
|
+
// `seo.llms.honeypot` is enabled. No human visits it, so any fetch is an
|
|
240
|
+
// automated reader that followed a link from the file. Always records (even
|
|
241
|
+
// if the flag was later turned off, a cached llms.txt may still point here)
|
|
242
|
+
// and returns a tiny noindex body.
|
|
243
|
+
export async function getAiCrawlerTrap(request) {
|
|
244
|
+
try {
|
|
245
|
+
const detected = detectAiBot(request.headers.get('user-agent'));
|
|
246
|
+
await recordAiCrawlerHit(db(), {
|
|
247
|
+
bot: detected?.token ?? 'unknown',
|
|
248
|
+
surface: 'honeypot',
|
|
249
|
+
category: detected?.category ?? 'other',
|
|
250
|
+
});
|
|
251
|
+
return new Response('This endpoint exists to detect automated indexers.\n', {
|
|
252
|
+
status: 200,
|
|
253
|
+
headers: {
|
|
254
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
255
|
+
'X-Robots-Tag': 'noindex',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return internalError(err, 'ai-crawlers/trap');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Admin: aggregated AI-crawler activity for the SEO → Technical panel.
|
|
264
|
+
// Any authenticated admin may read it (analytics, not a mutation).
|
|
265
|
+
export async function getAiCrawlers(request) {
|
|
266
|
+
try {
|
|
267
|
+
const auth = await requireAuth(request);
|
|
268
|
+
if (auth.error)
|
|
269
|
+
return auth.error;
|
|
270
|
+
if (!hasModel(db(), 'aiCrawlerHit')) {
|
|
271
|
+
return json({
|
|
272
|
+
data: { enabled: false, rows: [], totals: { hits: 0, bots: 0, honeypot: 0 } },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const rows = await db().aiCrawlerHit.findMany({
|
|
276
|
+
orderBy: { lastSeenAt: 'desc' },
|
|
277
|
+
take: 500,
|
|
278
|
+
});
|
|
279
|
+
const totalHits = rows.reduce((sum, r) => sum + Number(r.hits ?? 0), 0);
|
|
280
|
+
const honeypotHits = rows
|
|
281
|
+
.filter((r) => r.surface === 'honeypot')
|
|
282
|
+
.reduce((sum, r) => sum + Number(r.hits ?? 0), 0);
|
|
283
|
+
const uniqueBots = new Set(rows.map((r) => r.bot)).size;
|
|
284
|
+
// Recency windowing. `hits` is a lifetime cumulative counter (one row per
|
|
285
|
+
// bot×surface), so we can't derive per-window hit *counts* from it — but
|
|
286
|
+
// `lastSeenAt` lets us answer the actionable question "which bots/surfaces
|
|
287
|
+
// were active in the last 24h / 7d". Per-window hit-count trends would
|
|
288
|
+
// need a (bot, surface, day) bucket table (a future additive migration);
|
|
289
|
+
// this keeps storage bounded while making recency visible today.
|
|
290
|
+
const nowMs = Date.now();
|
|
291
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
292
|
+
const within = (v, ms) => {
|
|
293
|
+
if (!v)
|
|
294
|
+
return false;
|
|
295
|
+
const t = v instanceof Date ? v.getTime() : new Date(v).getTime();
|
|
296
|
+
return Number.isFinite(t) && nowMs - t <= ms;
|
|
297
|
+
};
|
|
298
|
+
const windowSummary = (ms) => {
|
|
299
|
+
const active = rows.filter((r) => within(r.lastSeenAt, ms));
|
|
300
|
+
return {
|
|
301
|
+
bots: new Set(active.map((r) => r.bot)).size,
|
|
302
|
+
surfaces: new Set(active.map((r) => r.surface)).size,
|
|
303
|
+
rows: active.length,
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
return json({
|
|
307
|
+
data: {
|
|
308
|
+
enabled: true,
|
|
309
|
+
rows: rows.map((r) => ({
|
|
310
|
+
bot: r.bot,
|
|
311
|
+
surface: r.surface,
|
|
312
|
+
category: r.category,
|
|
313
|
+
hits: Number(r.hits ?? 0),
|
|
314
|
+
firstSeenAt: r.firstSeenAt,
|
|
315
|
+
lastSeenAt: r.lastSeenAt,
|
|
316
|
+
activeLast24h: within(r.lastSeenAt, DAY_MS),
|
|
317
|
+
activeLast7d: within(r.lastSeenAt, 7 * DAY_MS),
|
|
318
|
+
})),
|
|
319
|
+
totals: { hits: totalHits, bots: uniqueBots, honeypot: honeypotHits },
|
|
320
|
+
windows: { last24h: windowSummary(DAY_MS), last7d: windowSummary(7 * DAY_MS) },
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return internalError(err, 'ai-crawlers');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=public-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-files.js","sourceRoot":"","sources":["../../../../../src/api/routes/seo/handlers/public-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { assertPlanFeature } from '../../../../plan/features.js';
|
|
2
|
+
import { guardedDb } from '../../../guarded-db.js';
|
|
3
|
+
import { type AuthSession } from '../../../route-helpers.js';
|
|
4
|
+
export declare const db: typeof guardedDb;
|
|
5
|
+
export declare const seoLogger: import("../../../../diagnostics/logger.js").ScopedLogger;
|
|
6
|
+
export declare function planRequiredResponse(denial: NonNullable<ReturnType<typeof assertPlanFeature>>): Response;
|
|
7
|
+
export declare function mapInternalLinkInsertError(err: unknown): Response | null;
|
|
8
|
+
export declare function seoAiGenerationError(result: {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}): Response;
|
|
12
|
+
/** Sort issues by risk (critical → warning → info), then most-recent first. */
|
|
13
|
+
export declare function sortIssuesBySeverity<T extends {
|
|
14
|
+
severity?: unknown;
|
|
15
|
+
lastDetectedAt?: unknown;
|
|
16
|
+
}>(issues: T[]): T[];
|
|
17
|
+
/**
|
|
18
|
+
* Content-writing fix strategies (`add-section`, `insert-link`, `fix-link`)
|
|
19
|
+
* write a document the route cannot name up front, so the API-key collection
|
|
20
|
+
* scope is checked inside `applyIssueFix` once the collection is known. JWT
|
|
21
|
+
* sessions already passed the route's `WRITE_ROLES` check and are never denied.
|
|
22
|
+
*/
|
|
23
|
+
export declare function contentWriteGate(session: AuthSession): (collection: string) => string | null;
|
|
24
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../../src/api/routes/seo/handlers/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAA;AAElC,eAAO,MAAM,EAAE,kBAAY,CAAA;AAK3B,eAAO,MAAM,SAAS,0DAAsB,CAAA;AAO5C,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,GACxD,QAAQ,CAUV;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAgBxE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,CAQxF;AACD,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,EAC7F,MAAM,EAAE,CAAC,EAAE,GACV,CAAC,EAAE,CAQL;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAE5F"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createLogger } from '../../../../diagnostics/logger.js';
|
|
2
|
+
import { guardedDb } from '../../../guarded-db.js';
|
|
3
|
+
import { apiKeyCollectionScopeDenial, errorResponse, json, } from '../../../route-helpers.js';
|
|
4
|
+
export const db = guardedDb;
|
|
5
|
+
// Explicit risk order. A plain `severity: 'asc'` DB sort orders the values
|
|
6
|
+
// alphabetically (critical, info, warning) which wrongly ranks `info` above
|
|
7
|
+
// `warning`. Unknown severities sort last.
|
|
8
|
+
export const seoLogger = createLogger('seo');
|
|
9
|
+
const SEVERITY_RANK = { critical: 0, warning: 1, info: 2 };
|
|
10
|
+
function severityRank(severity) {
|
|
11
|
+
return SEVERITY_RANK[String(severity)] ?? 99;
|
|
12
|
+
}
|
|
13
|
+
export function planRequiredResponse(denial) {
|
|
14
|
+
return json({
|
|
15
|
+
error: denial.message,
|
|
16
|
+
code: 'plan_required',
|
|
17
|
+
requiredTier: denial.requiredTier,
|
|
18
|
+
feature: denial.feature,
|
|
19
|
+
}, 403);
|
|
20
|
+
}
|
|
21
|
+
export function mapInternalLinkInsertError(err) {
|
|
22
|
+
const msg = err instanceof Error ? err.message : '';
|
|
23
|
+
switch (msg) {
|
|
24
|
+
case 'SOURCE_NOT_FOUND':
|
|
25
|
+
return errorResponse('Source document not found', 404);
|
|
26
|
+
case 'TARGET_NOT_FOUND':
|
|
27
|
+
return errorResponse('Target document not found', 404);
|
|
28
|
+
case 'NOT_WRAPPABLE':
|
|
29
|
+
return errorResponse('Anchor is not wrappable in the live source document.', 422);
|
|
30
|
+
case 'ALREADY_LINKED':
|
|
31
|
+
return errorResponse('Source already links to this target.', 422);
|
|
32
|
+
case 'TARGET_PATH_MISMATCH':
|
|
33
|
+
return errorResponse('Target path does not match the live document.', 422);
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function seoAiGenerationError(result) {
|
|
39
|
+
if (result.code === 'unavailable' || result.code === 'feature_disabled') {
|
|
40
|
+
return errorResponse(result.message, 503);
|
|
41
|
+
}
|
|
42
|
+
if (result.code === 'rate_limited' || result.code === 'budget_exceeded') {
|
|
43
|
+
return errorResponse(result.message, 429);
|
|
44
|
+
}
|
|
45
|
+
return errorResponse(result.message, 422);
|
|
46
|
+
}
|
|
47
|
+
/** Sort issues by risk (critical → warning → info), then most-recent first. */
|
|
48
|
+
export function sortIssuesBySeverity(issues) {
|
|
49
|
+
return [...issues].sort((a, b) => {
|
|
50
|
+
const rankDiff = severityRank(a.severity) - severityRank(b.severity);
|
|
51
|
+
if (rankDiff !== 0)
|
|
52
|
+
return rankDiff;
|
|
53
|
+
const at = a.lastDetectedAt ? new Date(a.lastDetectedAt).getTime() : 0;
|
|
54
|
+
const bt = b.lastDetectedAt ? new Date(b.lastDetectedAt).getTime() : 0;
|
|
55
|
+
return bt - at;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Content-writing fix strategies (`add-section`, `insert-link`, `fix-link`)
|
|
60
|
+
* write a document the route cannot name up front, so the API-key collection
|
|
61
|
+
* scope is checked inside `applyIssueFix` once the collection is known. JWT
|
|
62
|
+
* sessions already passed the route's `WRITE_ROLES` check and are never denied.
|
|
63
|
+
*/
|
|
64
|
+
export function contentWriteGate(session) {
|
|
65
|
+
return (collection) => apiKeyCollectionScopeDenial(session, collection, 'update');
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../../../src/api/routes/seo/handlers/shared.ts"],"names":[],"mappings":"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"}
|
package/dist/api/routes/seo.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ApiRouter } from '../router.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
export { sortIssuesBySeverity } from './seo/handlers/shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* SEO HTTP routes. Handlers live under `./seo/handlers` so this file stays a
|
|
5
|
+
* registration list. Request behavior is unchanged.
|
|
6
|
+
*/
|
|
7
7
|
export declare function registerSeoRoutes(router: ApiRouter): void;
|
|
8
8
|
//# sourceMappingURL=seo.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../../src/api/routes/seo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../../src/api/routes/seo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAmD7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAE/D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2CzD"}
|