@aeptus/aexeo-emdash 0.8.17 → 0.8.19
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 +129 -0
- package/INSTALL.md +22 -14
- package/README.md +6 -3
- package/dist/adapter.d.ts +32 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +550 -344
- package/dist/data-route.d.ts.map +1 -1
- package/dist/findings-view.d.ts +136 -0
- package/dist/findings-view.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -23
- package/dist/plugin.d.ts +25 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts +2 -22
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +745 -480
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sidecar-url.d.ts +18 -0
- package/dist/sidecar-url.d.ts.map +1 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +11 -5
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/dist/indexnow.d.ts +0 -15
- package/dist/indexnow.d.ts.map +0 -1
- package/dist/mcp.d.ts +0 -28
- package/dist/mcp.d.ts.map +0 -1
package/dist/configured.js
CHANGED
|
@@ -5,15 +5,42 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/adapter.ts
|
|
8
|
-
function
|
|
9
|
-
const
|
|
8
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
9
|
+
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
10
|
+
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
11
|
+
const adapted = adaptContentItem(content, {
|
|
12
|
+
route,
|
|
13
|
+
publicUrl,
|
|
14
|
+
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
15
|
+
});
|
|
16
|
+
const translationSet = await contentApi.getTranslations(
|
|
17
|
+
content.type,
|
|
18
|
+
content.id
|
|
19
|
+
);
|
|
20
|
+
const alternates = await Promise.all(
|
|
21
|
+
translationSet.translations.filter(
|
|
22
|
+
(translation) => translation.status === "published" && typeof translation.locale === "string" && translation.locale.length > 0
|
|
23
|
+
).map(async (translation) => {
|
|
24
|
+
const href = translation.id === content.id ? publicUrl : await contentApi.getPublicUrl(content.type, translation.id);
|
|
25
|
+
if (href === null) return null;
|
|
26
|
+
return { lang: translation.locale, href };
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
const uniqueAlternates = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const alternate of alternates) {
|
|
31
|
+
if (alternate !== null) uniqueAlternates.set(alternate.lang, alternate);
|
|
32
|
+
}
|
|
33
|
+
if (uniqueAlternates.size > 0) {
|
|
34
|
+
adapted.document.alternates = [...uniqueAlternates.values()];
|
|
35
|
+
}
|
|
36
|
+
return adapted;
|
|
37
|
+
}
|
|
38
|
+
function adaptContentItem(content, options = {}) {
|
|
39
|
+
const route = options.route ?? deriveRoute(content);
|
|
10
40
|
const title = stringOrEmpty(
|
|
11
41
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
12
42
|
);
|
|
13
|
-
const document = {
|
|
14
|
-
route,
|
|
15
|
-
title
|
|
16
|
-
};
|
|
43
|
+
const document = { route, title };
|
|
17
44
|
const description = content.seo?.description ?? content.data["description"];
|
|
18
45
|
if (typeof description === "string" && description.length > 0) {
|
|
19
46
|
document.description = description;
|
|
@@ -22,13 +49,12 @@ function adaptContentItem(content) {
|
|
|
22
49
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
23
50
|
document.canonical = canonical;
|
|
24
51
|
}
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
const locale = content.locale || options.defaultLocale;
|
|
53
|
+
if (typeof locale === "string" && locale.length > 0) {
|
|
54
|
+
document.lang = locale;
|
|
27
55
|
}
|
|
28
56
|
const body = extractPortableText(content.data);
|
|
29
|
-
if (body !== null)
|
|
30
|
-
document.body = body;
|
|
31
|
-
}
|
|
57
|
+
if (body !== null) document.body = body;
|
|
32
58
|
return {
|
|
33
59
|
document,
|
|
34
60
|
meta: {
|
|
@@ -36,49 +62,160 @@ function adaptContentItem(content) {
|
|
|
36
62
|
collection: content.type,
|
|
37
63
|
slug: content.slug,
|
|
38
64
|
status: content.status,
|
|
39
|
-
title
|
|
65
|
+
title,
|
|
66
|
+
publicUrl: options.publicUrl ?? null
|
|
40
67
|
}
|
|
41
68
|
};
|
|
42
69
|
}
|
|
43
70
|
function deriveRoute(content) {
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
return `/${encodeURIComponent(content.type)}/${encodeURIComponent(content.id)}`;
|
|
72
|
+
}
|
|
73
|
+
function routeFromPublicUrl(publicUrl) {
|
|
74
|
+
try {
|
|
75
|
+
return new URL(publicUrl).pathname || "/";
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
throw new Error(`EmDash returned an invalid public URL: ${publicUrl}`, {
|
|
78
|
+
cause
|
|
79
|
+
});
|
|
46
80
|
}
|
|
47
|
-
return `/${content.type}/${content.id}`;
|
|
48
81
|
}
|
|
49
82
|
function stringOrEmpty(value) {
|
|
50
83
|
return typeof value === "string" ? value : "";
|
|
51
84
|
}
|
|
52
85
|
function extractPortableText(data) {
|
|
53
|
-
const
|
|
54
|
-
for (const slug of candidates) {
|
|
86
|
+
for (const slug of ["body", "content"]) {
|
|
55
87
|
const value = data[slug];
|
|
56
|
-
if (isPortableTextArray(value))
|
|
57
|
-
|
|
88
|
+
if (isPortableTextArray(value)) return value;
|
|
89
|
+
if (isEmdashBlocksArray(value, slug)) return blocksToPortableText(value);
|
|
90
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
91
|
+
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
58
92
|
}
|
|
59
93
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
const blocks = data["blocks"];
|
|
95
|
+
if (isPortableTextArray(blocks)) return blocks;
|
|
96
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return blocksToPortableText(blocks);
|
|
97
|
+
for (const [field, value] of Object.entries(data)) {
|
|
98
|
+
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
99
|
+
if (isPortableTextArray(value)) return value;
|
|
100
|
+
if (isEmdashBlocksArray(value, field)) return blocksToPortableText(value);
|
|
64
101
|
}
|
|
65
102
|
return null;
|
|
66
103
|
}
|
|
67
104
|
function isPortableTextArray(value) {
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
return Array.isArray(value) && value.length > 0 && value.every(
|
|
106
|
+
(item) => isRecord(item) && item["_type"] === "block" && Array.isArray(item["children"]) && item["children"].every(
|
|
107
|
+
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
function isEmdashBlocksArray(value, field) {
|
|
112
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
113
|
+
return value.every(
|
|
114
|
+
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function blocksToPortableText(blocks) {
|
|
118
|
+
const paragraphs = [];
|
|
119
|
+
for (const block of blocks) {
|
|
120
|
+
const segments = [];
|
|
121
|
+
collectText(block, "", segments);
|
|
122
|
+
const text = segments.map((segment) => segment.trim()).filter(Boolean).join(" ");
|
|
123
|
+
if (text.length > 0) {
|
|
124
|
+
paragraphs.push(portableTextBlock(text, stringOrEmpty(block["_key"])));
|
|
125
|
+
}
|
|
70
126
|
}
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
return paragraphs;
|
|
128
|
+
}
|
|
129
|
+
function collectText(value, key, out) {
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
if (!isStructuralField(key) && value.trim().length > 0) out.push(value);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
for (const item of value) collectText(item, key, out);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(value)) return;
|
|
139
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
140
|
+
if (isStructuralField(childKey)) continue;
|
|
141
|
+
collectText(childValue, childKey, out);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
145
|
+
"_key",
|
|
146
|
+
"_ref",
|
|
147
|
+
"_type",
|
|
148
|
+
"_version",
|
|
149
|
+
"asset",
|
|
150
|
+
"align",
|
|
151
|
+
"alignment",
|
|
152
|
+
"autoplay",
|
|
153
|
+
"color",
|
|
154
|
+
"controls",
|
|
155
|
+
"focalpoint",
|
|
156
|
+
"height",
|
|
157
|
+
"href",
|
|
158
|
+
"id",
|
|
159
|
+
"layout",
|
|
160
|
+
"level",
|
|
161
|
+
"loop",
|
|
162
|
+
"markdefs",
|
|
163
|
+
"marks",
|
|
164
|
+
"mime_type",
|
|
165
|
+
"mimetype",
|
|
166
|
+
"position",
|
|
167
|
+
"rel",
|
|
168
|
+
"size",
|
|
169
|
+
"src",
|
|
170
|
+
"style",
|
|
171
|
+
"target",
|
|
172
|
+
"theme",
|
|
173
|
+
"tone",
|
|
174
|
+
"url",
|
|
175
|
+
"variant",
|
|
176
|
+
"width"
|
|
177
|
+
]);
|
|
178
|
+
function isStructuralField(key) {
|
|
179
|
+
const normalized = key.toLowerCase();
|
|
180
|
+
return normalized.startsWith("_") || STRUCTURAL_FIELDS.has(normalized);
|
|
181
|
+
}
|
|
182
|
+
function portableTextBlock(text, key) {
|
|
183
|
+
return {
|
|
184
|
+
_type: "block",
|
|
185
|
+
...key.length === 0 ? {} : { _key: key },
|
|
186
|
+
children: [
|
|
187
|
+
{
|
|
188
|
+
_type: "span",
|
|
189
|
+
...key.length === 0 ? {} : { _key: `${key}-text` },
|
|
190
|
+
text,
|
|
191
|
+
marks: []
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function isRecord(value) {
|
|
197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
198
|
}
|
|
74
199
|
|
|
75
200
|
// src/plugin.ts
|
|
201
|
+
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
202
|
+
var PLUGIN_SETTINGS_SCHEMA = {
|
|
203
|
+
[SETTINGS_TOKEN_KEY]: {
|
|
204
|
+
type: "secret",
|
|
205
|
+
label: "EVAL_TOKEN",
|
|
206
|
+
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
207
|
+
}
|
|
208
|
+
};
|
|
76
209
|
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter) {
|
|
77
|
-
const adapted =
|
|
210
|
+
const adapted = await adaptContentItemWithContext(
|
|
211
|
+
event.content,
|
|
212
|
+
ctx.content,
|
|
213
|
+
ctx.site?.locale
|
|
214
|
+
);
|
|
78
215
|
const document = adapted.document;
|
|
79
216
|
const { kv, log } = ctx;
|
|
80
217
|
const stored = { document, meta: adapted.meta };
|
|
81
|
-
await
|
|
218
|
+
await persistDocument(ctx, stored);
|
|
82
219
|
const result = await evaluator([document]);
|
|
83
220
|
if (!result.ok) {
|
|
84
221
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
@@ -125,7 +262,11 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
125
262
|
...cursor === void 0 ? {} : { cursor }
|
|
126
263
|
});
|
|
127
264
|
for (const item of page.items) {
|
|
128
|
-
const adapted =
|
|
265
|
+
const adapted = await adaptContentItemWithContext(
|
|
266
|
+
item,
|
|
267
|
+
ctx.content,
|
|
268
|
+
ctx.site?.locale
|
|
269
|
+
);
|
|
129
270
|
documents.push(adapted.document);
|
|
130
271
|
documentRoutes.add(adapted.document.route);
|
|
131
272
|
adaptedByRoute.set(adapted.document.route, {
|
|
@@ -177,8 +318,57 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
177
318
|
totalAfterSuppression += filtered.length;
|
|
178
319
|
}
|
|
179
320
|
summary.totalFindings = totalAfterSuppression;
|
|
321
|
+
if (summary.errors.length === 0) {
|
|
322
|
+
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
323
|
+
}
|
|
180
324
|
return summary;
|
|
181
325
|
}
|
|
326
|
+
async function persistDocument(ctx, stored) {
|
|
327
|
+
const route = stored.document.route;
|
|
328
|
+
const entries = await ctx.kv.list("document:");
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
const previous = toStoredDocument(entry.value);
|
|
331
|
+
if (previous === null || previous.document.route === route || previous.meta.id !== stored.meta.id || previous.meta.collection !== stored.meta.collection) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
await ctx.kv.delete(entry.key);
|
|
335
|
+
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
336
|
+
}
|
|
337
|
+
await ctx.kv.set(documentKey(route), stored);
|
|
338
|
+
}
|
|
339
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
340
|
+
const scannedCollections = new Set(collections);
|
|
341
|
+
const entries = await ctx.kv.list("document:");
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const stored = toStoredDocument(entry.value);
|
|
344
|
+
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
await ctx.kv.delete(entry.key);
|
|
348
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function toStoredDocument(value) {
|
|
352
|
+
if (value === null || typeof value !== "object") return null;
|
|
353
|
+
if ("document" in value && "meta" in value && value.document !== null && typeof value.document === "object" && "route" in value.document && typeof value.document.route === "string" && value.meta !== null && typeof value.meta === "object") {
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
if ("route" in value && typeof value.route === "string") {
|
|
357
|
+
const document = value;
|
|
358
|
+
return {
|
|
359
|
+
document,
|
|
360
|
+
meta: {
|
|
361
|
+
id: document.route,
|
|
362
|
+
collection: "",
|
|
363
|
+
slug: null,
|
|
364
|
+
status: "",
|
|
365
|
+
title: document.title ?? "",
|
|
366
|
+
publicUrl: null
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
182
372
|
function findingsKey(route) {
|
|
183
373
|
const normalized = route === "" || route === "/" ? "/" : route;
|
|
184
374
|
return `findings:${normalized}`;
|
|
@@ -199,34 +389,301 @@ async function readAllStoredDocuments(kv) {
|
|
|
199
389
|
const entries = await kv.list("document:");
|
|
200
390
|
const out = [];
|
|
201
391
|
for (const entry of entries) {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
392
|
+
const stored = toStoredDocument(entry.value);
|
|
393
|
+
if (stored !== null) out.push(stored);
|
|
394
|
+
}
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
async function readFindings(kv, route) {
|
|
398
|
+
const stored = await kv.get(findingsKey(route));
|
|
399
|
+
if (stored === null) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
return stored.findings;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/findings-view.ts
|
|
406
|
+
function normalizeInteraction(input) {
|
|
407
|
+
if (!input || typeof input !== "object") {
|
|
408
|
+
return { type: "page_load", page: "/findings" };
|
|
409
|
+
}
|
|
410
|
+
const raw = input;
|
|
411
|
+
if (raw.type === "page_load") {
|
|
412
|
+
return {
|
|
413
|
+
type: "page_load",
|
|
414
|
+
page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (raw.type === "block_action") {
|
|
418
|
+
return {
|
|
419
|
+
type: "block_action",
|
|
420
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
421
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
422
|
+
value: raw.value
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (raw.type === "form_submit") {
|
|
426
|
+
return {
|
|
427
|
+
type: "form_submit",
|
|
428
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
429
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
430
|
+
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return { type: "page_load", page: "/findings" };
|
|
434
|
+
}
|
|
435
|
+
async function handleRefresh(options) {
|
|
436
|
+
let summary;
|
|
437
|
+
try {
|
|
438
|
+
summary = await evaluateAndPersistAll(options.ctx, {
|
|
439
|
+
collections: options.collections,
|
|
440
|
+
evaluator: options.evaluator,
|
|
441
|
+
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
442
|
+
suppressionFilter: options.suppressionFilter
|
|
443
|
+
});
|
|
444
|
+
} catch (err) {
|
|
445
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
446
|
+
const message = `Refresh failed: ${detail}`;
|
|
447
|
+
return {
|
|
448
|
+
blocks: [
|
|
449
|
+
{ type: "header", text: "SEO findings" },
|
|
450
|
+
{
|
|
451
|
+
type: "banner",
|
|
452
|
+
title: message,
|
|
453
|
+
variant: options.tuning.refreshFailureVariant
|
|
454
|
+
}
|
|
455
|
+
],
|
|
456
|
+
toast: { message, type: "error" }
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
|
|
460
|
+
const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
|
|
461
|
+
if (summary.errors.length > 0) {
|
|
462
|
+
refreshed.blocks.unshift({
|
|
463
|
+
type: "banner",
|
|
464
|
+
title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
|
|
465
|
+
variant: "alert"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
...refreshed,
|
|
470
|
+
toast: {
|
|
471
|
+
message: toastMessage,
|
|
472
|
+
type: summary.errors.length === 0 ? "success" : "info"
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
async function readAllFindings(kv) {
|
|
477
|
+
const entries = await kv.list(
|
|
478
|
+
"findings:"
|
|
479
|
+
);
|
|
480
|
+
const out = [];
|
|
481
|
+
for (const entry of entries) {
|
|
482
|
+
if (entry.value === null) {
|
|
206
483
|
continue;
|
|
207
484
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
out.push({
|
|
211
|
-
document,
|
|
212
|
-
meta: {
|
|
213
|
-
id: document.route,
|
|
214
|
-
collection: "",
|
|
215
|
-
slug: null,
|
|
216
|
-
status: "",
|
|
217
|
-
title: document.title ?? ""
|
|
218
|
-
}
|
|
219
|
-
});
|
|
485
|
+
const route = entry.key.replace(/^findings:/, "");
|
|
486
|
+
for (const finding of entry.value.findings) {
|
|
487
|
+
out.push({ ...finding, document_route: route });
|
|
220
488
|
}
|
|
221
489
|
}
|
|
222
490
|
return out;
|
|
223
491
|
}
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
492
|
+
function severityFirst(a, b) {
|
|
493
|
+
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
494
|
+
const diff = rank(a.severity) - rank(b.severity);
|
|
495
|
+
if (diff !== 0) {
|
|
496
|
+
return diff;
|
|
497
|
+
}
|
|
498
|
+
return a.rule_id.localeCompare(b.rule_id);
|
|
499
|
+
}
|
|
500
|
+
function uniqueRoutes(rows) {
|
|
501
|
+
const routes = /* @__PURE__ */ new Set();
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
routes.add(row.document_route);
|
|
504
|
+
}
|
|
505
|
+
return [...routes].sort();
|
|
506
|
+
}
|
|
507
|
+
async function renderFindingsPage(kv, tuning) {
|
|
508
|
+
const findings = await readAllFindings(kv);
|
|
509
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
510
|
+
const warnings = findings.filter(
|
|
511
|
+
(finding) => finding.severity === "warning"
|
|
512
|
+
);
|
|
513
|
+
const sorted = [...findings].sort(severityFirst);
|
|
514
|
+
const routes = uniqueRoutes(findings);
|
|
515
|
+
const blocks = [
|
|
516
|
+
{ type: "header", text: "SEO findings" },
|
|
517
|
+
{
|
|
518
|
+
type: "context",
|
|
519
|
+
text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
520
|
+
},
|
|
521
|
+
{ type: "divider" },
|
|
522
|
+
{
|
|
523
|
+
type: "actions",
|
|
524
|
+
elements: [
|
|
525
|
+
// Refresh evaluates every configured entry. Pressing it lists
|
|
526
|
+
// content via the live in-request bridge, evaluates it, and
|
|
527
|
+
// writes findings back to KV (with suppressions applied).
|
|
528
|
+
{
|
|
529
|
+
type: "button",
|
|
530
|
+
label: "Refresh",
|
|
531
|
+
action_id: "refresh_findings",
|
|
532
|
+
style: "primary"
|
|
533
|
+
},
|
|
534
|
+
{ type: "button", label: "All", action_id: "filter:all" },
|
|
535
|
+
{ type: "button", label: "Errors only", action_id: "filter:errors" },
|
|
536
|
+
{
|
|
537
|
+
type: "button",
|
|
538
|
+
label: "Warnings only",
|
|
539
|
+
action_id: "filter:warnings"
|
|
540
|
+
}
|
|
541
|
+
]
|
|
542
|
+
},
|
|
543
|
+
sorted.length === 0 ? {
|
|
544
|
+
type: "context",
|
|
545
|
+
text: tuning.emptyFindingsText
|
|
546
|
+
} : findingsTable(sorted)
|
|
547
|
+
];
|
|
548
|
+
if (routes.length > 0) {
|
|
549
|
+
blocks.push({
|
|
550
|
+
type: "form",
|
|
551
|
+
fields: [
|
|
552
|
+
{
|
|
553
|
+
type: "select",
|
|
554
|
+
action_id: "route_picker",
|
|
555
|
+
label: "Document to inspect",
|
|
556
|
+
options: routes.map((route) => ({ label: route, value: route }))
|
|
557
|
+
}
|
|
558
|
+
],
|
|
559
|
+
submit: { label: "View document SEO", action_id: "view_document" }
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return { blocks };
|
|
563
|
+
}
|
|
564
|
+
function findingsTable(rows) {
|
|
565
|
+
return {
|
|
566
|
+
type: "table",
|
|
567
|
+
columns: [
|
|
568
|
+
{ key: "route", label: "Route" },
|
|
569
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
570
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
571
|
+
{ key: "message", label: "Message" }
|
|
572
|
+
],
|
|
573
|
+
rows: rows.map((row) => ({
|
|
574
|
+
route: row.document_route,
|
|
575
|
+
rule: row.rule_id,
|
|
576
|
+
severity: row.severity,
|
|
577
|
+
message: row.message
|
|
578
|
+
}))
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function renderScoreWidget(kv, tuning) {
|
|
582
|
+
const documents = await readAllDocuments(kv);
|
|
583
|
+
if (documents.length === 0) {
|
|
584
|
+
return {
|
|
585
|
+
blocks: [
|
|
586
|
+
{ type: "header", text: "SEO score" },
|
|
587
|
+
{
|
|
588
|
+
type: "context",
|
|
589
|
+
text: tuning.emptyScoreText
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const score = await tuning.score(documents, kv);
|
|
595
|
+
const blocks = [
|
|
596
|
+
{
|
|
597
|
+
type: "stats",
|
|
598
|
+
items: [
|
|
599
|
+
{ label: "Overall", value: `${score.overall_score}` },
|
|
600
|
+
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
601
|
+
{
|
|
602
|
+
label: tuning.truthLabel(score),
|
|
603
|
+
value: `${score.truth_consistency_score}`
|
|
604
|
+
},
|
|
605
|
+
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
];
|
|
609
|
+
if (score.overall_score < 60) {
|
|
610
|
+
blocks.unshift({
|
|
611
|
+
type: "banner",
|
|
612
|
+
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
613
|
+
variant: "alert"
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
if (score.blockers.length > 0) {
|
|
617
|
+
blocks.push({
|
|
618
|
+
type: "context",
|
|
619
|
+
text: topBlockersLine(score)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return { blocks };
|
|
623
|
+
}
|
|
624
|
+
function topBlockersLine(score) {
|
|
625
|
+
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
626
|
+
if (top.length === 0) {
|
|
627
|
+
return "No blockers identified.";
|
|
628
|
+
}
|
|
629
|
+
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
630
|
+
}
|
|
631
|
+
async function renderDocumentPanel(kv, route) {
|
|
632
|
+
if (route === void 0) {
|
|
633
|
+
return {
|
|
634
|
+
blocks: [
|
|
635
|
+
{ type: "header", text: "Document SEO" },
|
|
636
|
+
{
|
|
637
|
+
type: "context",
|
|
638
|
+
text: "Pick a document from the SEO findings list to see its rule findings here."
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const findings = await readFindings(kv, route);
|
|
644
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
645
|
+
const warnings = findings.filter(
|
|
646
|
+
(finding) => finding.severity === "warning"
|
|
647
|
+
);
|
|
648
|
+
const sorted = [...findings].sort(severityFirst);
|
|
649
|
+
return {
|
|
650
|
+
blocks: [
|
|
651
|
+
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
652
|
+
{
|
|
653
|
+
type: "context",
|
|
654
|
+
text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
655
|
+
},
|
|
656
|
+
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
657
|
+
]
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function documentFindingsTable(findings) {
|
|
661
|
+
return {
|
|
662
|
+
type: "table",
|
|
663
|
+
columns: [
|
|
664
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
665
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
666
|
+
{ key: "message", label: "Message" },
|
|
667
|
+
{ key: "block", label: "Block", format: "code" }
|
|
668
|
+
],
|
|
669
|
+
rows: findings.map((finding) => ({
|
|
670
|
+
rule: finding.rule_id,
|
|
671
|
+
severity: finding.severity,
|
|
672
|
+
message: finding.message,
|
|
673
|
+
// The bridge stamps every Portable Text block with id + data-pt-key
|
|
674
|
+
// when rendering; surfacing the path:line locator here lets authors
|
|
675
|
+
// locate the failing block via the editor's ⌘-F.
|
|
676
|
+
block: `${finding.path}:${finding.line}`
|
|
677
|
+
}))
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function notFound(page) {
|
|
681
|
+
return {
|
|
682
|
+
blocks: [
|
|
683
|
+
{ type: "header", text: "Not found" },
|
|
684
|
+
{ type: "context", text: `unknown page: ${page}` }
|
|
685
|
+
]
|
|
686
|
+
};
|
|
230
687
|
}
|
|
231
688
|
|
|
232
689
|
// wasm/aexeo_emdash_bridge_bg.js
|
|
@@ -559,7 +1016,6 @@ async function buildFindingsPayload(ctx) {
|
|
|
559
1016
|
}
|
|
560
1017
|
}
|
|
561
1018
|
findings.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
562
|
-
const siteUrl = ctx.site?.url ?? "";
|
|
563
1019
|
const routes = [];
|
|
564
1020
|
for (const [route, bucket] of perRoute) {
|
|
565
1021
|
const stored2 = documents.get(route);
|
|
@@ -570,7 +1026,7 @@ async function buildFindingsPayload(ctx) {
|
|
|
570
1026
|
collection: stored2?.meta.collection ?? "",
|
|
571
1027
|
id: stored2?.meta.id ?? "",
|
|
572
1028
|
editUrl: stored2 ? buildEditUrl(stored2.meta) : "",
|
|
573
|
-
publishedUrl: stored2
|
|
1029
|
+
publishedUrl: stored2?.meta.publicUrl ?? "",
|
|
574
1030
|
findingCount: bucket.findings.length,
|
|
575
1031
|
errorCount: bucket.errors,
|
|
576
1032
|
warningCount: bucket.warnings
|
|
@@ -696,12 +1152,6 @@ function buildEditUrl(meta) {
|
|
|
696
1152
|
if (meta.collection === "" || meta.id === "") return "";
|
|
697
1153
|
return `/_emdash/admin/content/${encodeURIComponent(meta.collection)}/${encodeURIComponent(meta.id)}`;
|
|
698
1154
|
}
|
|
699
|
-
function buildPublishedUrl(meta, route, siteUrl) {
|
|
700
|
-
if (meta.status !== "published") return "";
|
|
701
|
-
if (siteUrl === "") return "";
|
|
702
|
-
const trimmed = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl;
|
|
703
|
-
return `${trimmed}${route}`;
|
|
704
|
-
}
|
|
705
1155
|
|
|
706
1156
|
// src/facts-route.ts
|
|
707
1157
|
import { PluginRouteError } from "emdash";
|
|
@@ -854,11 +1304,11 @@ async function checkWikipedia(input, fetcher) {
|
|
|
854
1304
|
const descriptions = body[2];
|
|
855
1305
|
const urls = body[3];
|
|
856
1306
|
if (!Array.isArray(titles) || !Array.isArray(urls) || titles.length === 0) {
|
|
857
|
-
return
|
|
1307
|
+
return notFound2("wikipedia");
|
|
858
1308
|
}
|
|
859
1309
|
const title = String(titles[0]);
|
|
860
1310
|
if (!fuzzyMatch(title, input.name)) {
|
|
861
|
-
return
|
|
1311
|
+
return notFound2("wikipedia");
|
|
862
1312
|
}
|
|
863
1313
|
const articleUrl = String(urls[0] ?? "");
|
|
864
1314
|
const description = Array.isArray(descriptions) ? String(descriptions[0] ?? "") : "";
|
|
@@ -884,11 +1334,11 @@ async function checkWikidata(input, fetcher) {
|
|
|
884
1334
|
const body = await res.json();
|
|
885
1335
|
const top = body.search?.[0];
|
|
886
1336
|
if (!top || typeof top.id !== "string") {
|
|
887
|
-
return
|
|
1337
|
+
return notFound2("wikidata");
|
|
888
1338
|
}
|
|
889
1339
|
const label = top.label ?? input.name;
|
|
890
1340
|
if (!fuzzyMatch(label, input.name)) {
|
|
891
|
-
return
|
|
1341
|
+
return notFound2("wikidata");
|
|
892
1342
|
}
|
|
893
1343
|
return found("wikidata", {
|
|
894
1344
|
label: `${top.id}${top.label ? ` \u2014 ${top.label}` : ""}`,
|
|
@@ -914,7 +1364,7 @@ async function checkGitHub(input, fetcher) {
|
|
|
914
1364
|
// it the API can return either v3 or experimental shapes.
|
|
915
1365
|
headers: { Accept: "application/vnd.github+json" }
|
|
916
1366
|
});
|
|
917
|
-
if (res.status === 404) return
|
|
1367
|
+
if (res.status === 404) return notFound2("github");
|
|
918
1368
|
if (res.status === 403 || res.status === 429) {
|
|
919
1369
|
return unreachable("github", "rate-limited (try Refresh later)");
|
|
920
1370
|
}
|
|
@@ -946,7 +1396,7 @@ async function checkRdap(input, fetcher) {
|
|
|
946
1396
|
const url = `https://rdap.org/domain/${encodeURIComponent(host)}`;
|
|
947
1397
|
try {
|
|
948
1398
|
const res = await timedFetch(fetcher, url);
|
|
949
|
-
if (res.status === 404) return
|
|
1399
|
+
if (res.status === 404) return notFound2("rdap");
|
|
950
1400
|
if (!res.ok) return unreachable("rdap", `HTTP ${res.status}`);
|
|
951
1401
|
const body = await res.json();
|
|
952
1402
|
const reg = (body.events ?? []).find(
|
|
@@ -983,13 +1433,13 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
983
1433
|
url.searchParams.set("limit", "1");
|
|
984
1434
|
try {
|
|
985
1435
|
const res = await timedFetch(fetcher, url.toString());
|
|
986
|
-
if (res.status === 404) return
|
|
1436
|
+
if (res.status === 404) return notFound2("common_crawl");
|
|
987
1437
|
if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
|
|
988
1438
|
const text = (await res.text()).trim();
|
|
989
|
-
if (text.length === 0) return
|
|
1439
|
+
if (text.length === 0) return notFound2("common_crawl");
|
|
990
1440
|
let firstLine = text.split("\n")[0];
|
|
991
1441
|
if (firstLine === void 0 || firstLine.length === 0) {
|
|
992
|
-
return
|
|
1442
|
+
return notFound2("common_crawl");
|
|
993
1443
|
}
|
|
994
1444
|
let entry;
|
|
995
1445
|
try {
|
|
@@ -1031,7 +1481,7 @@ function found(source, details) {
|
|
|
1031
1481
|
if (details.extra !== void 0) result.extra = details.extra;
|
|
1032
1482
|
return result;
|
|
1033
1483
|
}
|
|
1034
|
-
function
|
|
1484
|
+
function notFound2(source) {
|
|
1035
1485
|
return { source, status: "not_found", checkedAt: nowIso() };
|
|
1036
1486
|
}
|
|
1037
1487
|
function unreachable(source, error) {
|
|
@@ -1100,7 +1550,7 @@ async function handlePresenceRoute(ctx) {
|
|
|
1100
1550
|
case "data":
|
|
1101
1551
|
return await handleData2(ctx);
|
|
1102
1552
|
case "refresh":
|
|
1103
|
-
return await
|
|
1553
|
+
return await handleRefresh2(ctx);
|
|
1104
1554
|
default:
|
|
1105
1555
|
throw PluginRouteError2.badRequest(
|
|
1106
1556
|
`unknown presence route kind: ${String(body.kind)}`
|
|
@@ -1127,7 +1577,7 @@ async function handleData2(ctx) {
|
|
|
1127
1577
|
const state = ageMs !== null && ageMs > CACHE_TTL_MS ? "stale" : "fresh";
|
|
1128
1578
|
return payload(state, cached.results, cached.generatedAt, entity.name);
|
|
1129
1579
|
}
|
|
1130
|
-
async function
|
|
1580
|
+
async function handleRefresh2(ctx) {
|
|
1131
1581
|
const manifest = await readStoredFacts(ctx.kv);
|
|
1132
1582
|
if (manifest === null) {
|
|
1133
1583
|
return payload("no_manifest", [], null, null);
|
|
@@ -1215,7 +1665,7 @@ function globToMatcher(pattern) {
|
|
|
1215
1665
|
}
|
|
1216
1666
|
|
|
1217
1667
|
// src/version.ts
|
|
1218
|
-
var PACKAGE_VERSION = "0.8.
|
|
1668
|
+
var PACKAGE_VERSION = "0.8.19";
|
|
1219
1669
|
|
|
1220
1670
|
// src/configured.ts
|
|
1221
1671
|
import { definePlugin } from "emdash";
|
|
@@ -1284,7 +1734,7 @@ function createPlugin(options = {}) {
|
|
|
1284
1734
|
return definePlugin({
|
|
1285
1735
|
id: "aexeo-emdash",
|
|
1286
1736
|
version: PACKAGE_VERSION,
|
|
1287
|
-
capabilities: ["read
|
|
1737
|
+
capabilities: ["content:read"],
|
|
1288
1738
|
hooks: {
|
|
1289
1739
|
// afterSave processes one saved document at a time — collections
|
|
1290
1740
|
// list isn't needed here. The hook always runs regardless of
|
|
@@ -1301,7 +1751,7 @@ function createPlugin(options = {}) {
|
|
|
1301
1751
|
// one consumer of `collections`. Bind the resolved list here so
|
|
1302
1752
|
// every request handles the same set without re-reading options.
|
|
1303
1753
|
admin: {
|
|
1304
|
-
handler: (ctx) => handleAdminRoute(ctx, collections)
|
|
1754
|
+
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter)
|
|
1305
1755
|
},
|
|
1306
1756
|
// JSON data endpoint for the React adminEntry. Two routes:
|
|
1307
1757
|
// POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
|
|
@@ -1371,246 +1821,59 @@ function createPlugin(options = {}) {
|
|
|
1371
1821
|
});
|
|
1372
1822
|
}
|
|
1373
1823
|
var configured_default = createPlugin;
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
if (raw.type === "block_action") {
|
|
1386
|
-
return {
|
|
1387
|
-
type: "block_action",
|
|
1388
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
1389
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
1390
|
-
value: raw.value
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
if (raw.type === "form_submit") {
|
|
1394
|
-
return {
|
|
1395
|
-
type: "form_submit",
|
|
1396
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
1397
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
1398
|
-
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
return { type: "page_load", page: "/findings" };
|
|
1402
|
-
}
|
|
1403
|
-
async function handleAdminRoute(ctx, collections) {
|
|
1824
|
+
var adminViewTuning = {
|
|
1825
|
+
emptyFindingsText: "Once a Refresh runs, findings will list here.",
|
|
1826
|
+
emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
|
|
1827
|
+
refreshFailureVariant: "error",
|
|
1828
|
+
score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
|
|
1829
|
+
truthLabel: formatTruthLabel
|
|
1830
|
+
};
|
|
1831
|
+
async function handleAdminRoute(ctx, collections, suppressionFilter) {
|
|
1404
1832
|
const body = normalizeInteraction(ctx.input);
|
|
1405
1833
|
if (body.type === "page_load") {
|
|
1406
1834
|
return handlePageLoad(ctx, body.page);
|
|
1407
1835
|
}
|
|
1408
1836
|
if (body.type === "block_action") {
|
|
1409
1837
|
if (body.action_id === "view_document" && typeof body.value === "string") {
|
|
1410
|
-
return renderDocumentPanel(ctx, body.value);
|
|
1838
|
+
return renderDocumentPanel(ctx.kv, body.value);
|
|
1411
1839
|
}
|
|
1412
1840
|
if (body.action_id === "refresh_findings") {
|
|
1413
|
-
return
|
|
1841
|
+
return handleRefresh({
|
|
1842
|
+
ctx,
|
|
1843
|
+
collections,
|
|
1844
|
+
evaluator: inProcessEvaluator,
|
|
1845
|
+
suppressionFilter,
|
|
1846
|
+
tuning: adminViewTuning
|
|
1847
|
+
});
|
|
1414
1848
|
}
|
|
1415
1849
|
if (body.action_id.startsWith("filter:")) {
|
|
1416
|
-
return renderFindingsPage(ctx);
|
|
1850
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1417
1851
|
}
|
|
1418
|
-
return
|
|
1852
|
+
return notFound(body.action_id);
|
|
1419
1853
|
}
|
|
1420
1854
|
if (body.type === "form_submit") {
|
|
1421
1855
|
if (body.action_id === "view_document") {
|
|
1422
1856
|
const picked = body.values["route_picker"];
|
|
1423
1857
|
if (typeof picked === "string" && picked.length > 0) {
|
|
1424
|
-
return renderDocumentPanel(ctx, picked);
|
|
1858
|
+
return renderDocumentPanel(ctx.kv, picked);
|
|
1425
1859
|
}
|
|
1426
1860
|
}
|
|
1427
|
-
return renderFindingsPage(ctx);
|
|
1861
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1428
1862
|
}
|
|
1429
|
-
return renderFindingsPage(ctx);
|
|
1863
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1430
1864
|
}
|
|
1431
1865
|
async function handlePageLoad(ctx, page) {
|
|
1432
1866
|
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
1433
1867
|
if (normalized === "" || normalized === "findings") {
|
|
1434
|
-
return renderFindingsPage(ctx);
|
|
1868
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1435
1869
|
}
|
|
1436
1870
|
if (normalized === "widget:aexeo-score") {
|
|
1437
|
-
return renderScoreWidget(ctx);
|
|
1871
|
+
return renderScoreWidget(ctx.kv, adminViewTuning);
|
|
1438
1872
|
}
|
|
1439
1873
|
if (normalized === "document") {
|
|
1440
|
-
return renderDocumentPanel(ctx);
|
|
1441
|
-
}
|
|
1442
|
-
return notFound2(page);
|
|
1443
|
-
}
|
|
1444
|
-
async function handleRefresh2(ctx, collections) {
|
|
1445
|
-
let summary;
|
|
1446
|
-
try {
|
|
1447
|
-
summary = await evaluateAndPersistAll(ctx, {
|
|
1448
|
-
evaluator: inProcessEvaluator,
|
|
1449
|
-
collections
|
|
1450
|
-
});
|
|
1451
|
-
} catch (err) {
|
|
1452
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1453
|
-
return {
|
|
1454
|
-
blocks: [
|
|
1455
|
-
{ type: "header", text: "SEO findings" },
|
|
1456
|
-
{
|
|
1457
|
-
type: "banner",
|
|
1458
|
-
title: `Refresh failed: ${detail}`,
|
|
1459
|
-
variant: "error"
|
|
1460
|
-
}
|
|
1461
|
-
],
|
|
1462
|
-
toast: { message: `Refresh failed: ${detail}`, type: "error" }
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
const refreshed = await renderFindingsPage(ctx);
|
|
1466
|
-
const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
|
|
1467
|
-
if (summary.errors.length > 0) {
|
|
1468
|
-
refreshed.blocks.unshift({
|
|
1469
|
-
type: "banner",
|
|
1470
|
-
title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
|
|
1471
|
-
variant: "alert"
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
return {
|
|
1475
|
-
...refreshed,
|
|
1476
|
-
toast: {
|
|
1477
|
-
message: toastMessage,
|
|
1478
|
-
type: summary.errors.length === 0 ? "success" : "info"
|
|
1479
|
-
}
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
async function readAllFindings(ctx) {
|
|
1483
|
-
const entries = await ctx.kv.list(
|
|
1484
|
-
"findings:"
|
|
1485
|
-
);
|
|
1486
|
-
const out = [];
|
|
1487
|
-
for (const entry of entries) {
|
|
1488
|
-
if (entry.value === null) continue;
|
|
1489
|
-
const route = entry.key.replace(/^findings:/, "");
|
|
1490
|
-
for (const finding of entry.value.findings) {
|
|
1491
|
-
out.push({ ...finding, document_route: route });
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
return out;
|
|
1495
|
-
}
|
|
1496
|
-
function severityFirst(a, b) {
|
|
1497
|
-
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
1498
|
-
const diff = rank(a.severity) - rank(b.severity);
|
|
1499
|
-
if (diff !== 0) return diff;
|
|
1500
|
-
return a.rule_id.localeCompare(b.rule_id);
|
|
1501
|
-
}
|
|
1502
|
-
function uniqueRoutes(rows) {
|
|
1503
|
-
const set = /* @__PURE__ */ new Set();
|
|
1504
|
-
for (const row of rows) set.add(row.document_route);
|
|
1505
|
-
return [...set].sort();
|
|
1506
|
-
}
|
|
1507
|
-
async function renderFindingsPage(ctx) {
|
|
1508
|
-
const findings = await readAllFindings(ctx);
|
|
1509
|
-
const errors = findings.filter((f) => f.severity === "error");
|
|
1510
|
-
const warnings = findings.filter((f) => f.severity === "warning");
|
|
1511
|
-
const sorted = [...findings].sort(severityFirst);
|
|
1512
|
-
const routes = uniqueRoutes(findings);
|
|
1513
|
-
const blocks = [
|
|
1514
|
-
{ type: "header", text: "SEO findings" },
|
|
1515
|
-
{
|
|
1516
|
-
type: "context",
|
|
1517
|
-
text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1518
|
-
},
|
|
1519
|
-
{ type: "divider" },
|
|
1520
|
-
{
|
|
1521
|
-
type: "actions",
|
|
1522
|
-
elements: [
|
|
1523
|
-
{
|
|
1524
|
-
type: "button",
|
|
1525
|
-
label: "Refresh",
|
|
1526
|
-
action_id: "refresh_findings",
|
|
1527
|
-
style: "primary"
|
|
1528
|
-
},
|
|
1529
|
-
{ type: "button", label: "All", action_id: "filter:all" },
|
|
1530
|
-
{ type: "button", label: "Errors only", action_id: "filter:errors" },
|
|
1531
|
-
{
|
|
1532
|
-
type: "button",
|
|
1533
|
-
label: "Warnings only",
|
|
1534
|
-
action_id: "filter:warnings"
|
|
1535
|
-
}
|
|
1536
|
-
]
|
|
1537
|
-
},
|
|
1538
|
-
sorted.length === 0 ? {
|
|
1539
|
-
type: "context",
|
|
1540
|
-
text: "Once a Refresh runs, findings will list here."
|
|
1541
|
-
} : findingsTable(sorted)
|
|
1542
|
-
];
|
|
1543
|
-
if (routes.length > 0) {
|
|
1544
|
-
blocks.push({
|
|
1545
|
-
type: "form",
|
|
1546
|
-
fields: [
|
|
1547
|
-
{
|
|
1548
|
-
type: "select",
|
|
1549
|
-
action_id: "route_picker",
|
|
1550
|
-
label: "Document to inspect",
|
|
1551
|
-
options: routes.map((route) => ({ label: route, value: route }))
|
|
1552
|
-
}
|
|
1553
|
-
],
|
|
1554
|
-
submit: { label: "View document SEO", action_id: "view_document" }
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
1557
|
-
return { blocks };
|
|
1558
|
-
}
|
|
1559
|
-
function findingsTable(rows) {
|
|
1560
|
-
return {
|
|
1561
|
-
type: "table",
|
|
1562
|
-
columns: [
|
|
1563
|
-
{ key: "route", label: "Route" },
|
|
1564
|
-
{ key: "rule", label: "Rule", format: "code" },
|
|
1565
|
-
{ key: "severity", label: "Severity", format: "badge" },
|
|
1566
|
-
{ key: "message", label: "Message" }
|
|
1567
|
-
],
|
|
1568
|
-
rows: rows.map((row) => ({
|
|
1569
|
-
route: row.document_route,
|
|
1570
|
-
rule: row.rule_id,
|
|
1571
|
-
severity: row.severity,
|
|
1572
|
-
message: row.message
|
|
1573
|
-
}))
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
async function renderScoreWidget(ctx) {
|
|
1577
|
-
const documents = await readAllDocuments(ctx.kv);
|
|
1578
|
-
if (documents.length === 0) {
|
|
1579
|
-
return {
|
|
1580
|
-
blocks: [
|
|
1581
|
-
{ type: "header", text: "SEO score" },
|
|
1582
|
-
{
|
|
1583
|
-
type: "context",
|
|
1584
|
-
text: "No documents indexed \u2014 click Refresh on the Aexeo findings page."
|
|
1585
|
-
}
|
|
1586
|
-
]
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
const facts = await readStoredFacts(ctx.kv);
|
|
1590
|
-
const score = await scoreLocally(documents, facts);
|
|
1591
|
-
const truthLabel = formatTruthLabel(score);
|
|
1592
|
-
const blocks = [
|
|
1593
|
-
{
|
|
1594
|
-
type: "stats",
|
|
1595
|
-
items: [
|
|
1596
|
-
{ label: "Overall", value: `${score.overall_score}` },
|
|
1597
|
-
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
1598
|
-
{ label: truthLabel, value: `${score.truth_consistency_score}` },
|
|
1599
|
-
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
1600
|
-
]
|
|
1601
|
-
}
|
|
1602
|
-
];
|
|
1603
|
-
if (score.overall_score < 60) {
|
|
1604
|
-
blocks.unshift({
|
|
1605
|
-
type: "banner",
|
|
1606
|
-
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
1607
|
-
variant: "alert"
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
if (score.blockers.length > 0) {
|
|
1611
|
-
blocks.push({ type: "context", text: topBlockersLine(score) });
|
|
1874
|
+
return renderDocumentPanel(ctx.kv);
|
|
1612
1875
|
}
|
|
1613
|
-
return
|
|
1876
|
+
return notFound(page);
|
|
1614
1877
|
}
|
|
1615
1878
|
async function scoreLocally(documents, manifest = null) {
|
|
1616
1879
|
const manifestJson = manifest === null ? null : JSON.stringify(manifest);
|
|
@@ -1631,63 +1894,6 @@ function formatTruthLabel(score) {
|
|
|
1631
1894
|
return "Truth";
|
|
1632
1895
|
}
|
|
1633
1896
|
}
|
|
1634
|
-
function topBlockersLine(score) {
|
|
1635
|
-
const top = score.blockers.slice(0, 3).map((b) => b.message);
|
|
1636
|
-
if (top.length === 0) return "No blockers identified.";
|
|
1637
|
-
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
1638
|
-
}
|
|
1639
|
-
async function renderDocumentPanel(ctx, route) {
|
|
1640
|
-
if (route === void 0) {
|
|
1641
|
-
return {
|
|
1642
|
-
blocks: [
|
|
1643
|
-
{ type: "header", text: "Document SEO" },
|
|
1644
|
-
{
|
|
1645
|
-
type: "context",
|
|
1646
|
-
text: "Pick a document from the SEO findings list to see its rule findings here."
|
|
1647
|
-
}
|
|
1648
|
-
]
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
const findings = await readFindings(ctx.kv, route);
|
|
1652
|
-
const errors = findings.filter((f) => f.severity === "error");
|
|
1653
|
-
const warnings = findings.filter((f) => f.severity === "warning");
|
|
1654
|
-
const sorted = [...findings].sort(severityFirst);
|
|
1655
|
-
return {
|
|
1656
|
-
blocks: [
|
|
1657
|
-
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
1658
|
-
{
|
|
1659
|
-
type: "context",
|
|
1660
|
-
text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1661
|
-
},
|
|
1662
|
-
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
1663
|
-
]
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function documentFindingsTable(findings) {
|
|
1667
|
-
return {
|
|
1668
|
-
type: "table",
|
|
1669
|
-
columns: [
|
|
1670
|
-
{ key: "rule", label: "Rule", format: "code" },
|
|
1671
|
-
{ key: "severity", label: "Severity", format: "badge" },
|
|
1672
|
-
{ key: "message", label: "Message" },
|
|
1673
|
-
{ key: "block", label: "Block", format: "code" }
|
|
1674
|
-
],
|
|
1675
|
-
rows: findings.map((finding) => ({
|
|
1676
|
-
rule: finding.rule_id,
|
|
1677
|
-
severity: finding.severity,
|
|
1678
|
-
message: finding.message,
|
|
1679
|
-
block: `${finding.path}:${finding.line}`
|
|
1680
|
-
}))
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
function notFound2(page) {
|
|
1684
|
-
return {
|
|
1685
|
-
blocks: [
|
|
1686
|
-
{ type: "header", text: "Not found" },
|
|
1687
|
-
{ type: "context", text: `unknown page: ${page}` }
|
|
1688
|
-
]
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
1897
|
export {
|
|
1692
1898
|
createPlugin,
|
|
1693
1899
|
configured_default as default
|