@aeptus/aexeo-emdash 0.1.0
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 +45 -0
- package/INSTALL.md +289 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/adapter.d.ts +16 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/configured.d.ts +5 -0
- package/dist/configured.d.ts.map +1 -0
- package/dist/configured.js +661 -0
- package/dist/evaluator-sandbox.d.ts +6 -0
- package/dist/evaluator-sandbox.d.ts.map +1 -0
- package/dist/evaluator.d.ts +9 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/indexnow.d.ts +15 -0
- package/dist/indexnow.d.ts.map +1 -0
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/plugin.d.ts +77 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/sandbox-entry.d.ts +47 -0
- package/dist/sandbox-entry.d.ts.map +1 -0
- package/dist/sandbox-entry.js +846 -0
- package/dist/sandbox.d.ts +25 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sidecar.d.ts +27 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/wasm-init.d.ts +3 -0
- package/dist/wasm-init.d.ts.map +1 -0
- package/package.json +78 -0
- package/wasm/aexeo_emdash_bridge.d.ts +6 -0
- package/wasm/aexeo_emdash_bridge.js +9 -0
- package/wasm/aexeo_emdash_bridge_bg.d.ts +17 -0
- package/wasm/aexeo_emdash_bridge_bg.js +159 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +11 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/adapter.ts
|
|
8
|
+
function contentItemToEmdashDocument(content) {
|
|
9
|
+
const route = deriveRoute(content);
|
|
10
|
+
const title = stringOrEmpty(
|
|
11
|
+
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
12
|
+
);
|
|
13
|
+
const document = {
|
|
14
|
+
route,
|
|
15
|
+
title
|
|
16
|
+
};
|
|
17
|
+
const description = content.seo?.description ?? content.data["description"];
|
|
18
|
+
if (typeof description === "string" && description.length > 0) {
|
|
19
|
+
document.description = description;
|
|
20
|
+
}
|
|
21
|
+
const canonical = content.seo?.canonical;
|
|
22
|
+
if (typeof canonical === "string" && canonical.length > 0) {
|
|
23
|
+
document.canonical = canonical;
|
|
24
|
+
}
|
|
25
|
+
if (content.locale !== null) {
|
|
26
|
+
document.lang = content.locale;
|
|
27
|
+
}
|
|
28
|
+
const body = extractPortableText(content.data);
|
|
29
|
+
if (body !== null) {
|
|
30
|
+
document.body = body;
|
|
31
|
+
}
|
|
32
|
+
return document;
|
|
33
|
+
}
|
|
34
|
+
function deriveRoute(content) {
|
|
35
|
+
if (typeof content.slug === "string" && content.slug.length > 0) {
|
|
36
|
+
return content.slug.startsWith("/") ? content.slug : `/${content.slug}`;
|
|
37
|
+
}
|
|
38
|
+
return `/${content.type}/${content.id}`;
|
|
39
|
+
}
|
|
40
|
+
function stringOrEmpty(value) {
|
|
41
|
+
return typeof value === "string" ? value : "";
|
|
42
|
+
}
|
|
43
|
+
function extractPortableText(data) {
|
|
44
|
+
const candidates = ["body", "content"];
|
|
45
|
+
for (const slug of candidates) {
|
|
46
|
+
const value = data[slug];
|
|
47
|
+
if (isPortableTextArray(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const value of Object.values(data)) {
|
|
52
|
+
if (isPortableTextArray(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function isPortableTextArray(value) {
|
|
59
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const first = value[0];
|
|
63
|
+
return typeof first === "object" && first !== null && "_type" in first && typeof first._type === "string";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/plugin.ts
|
|
67
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator) {
|
|
68
|
+
const document = contentItemToEmdashDocument(event.content);
|
|
69
|
+
const { kv, log } = ctx;
|
|
70
|
+
await kv.set(documentKey(document.route), document);
|
|
71
|
+
const result = await evaluator([document]);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
74
|
+
detail: result.detail
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const pageFindings = result.findings.filter(
|
|
79
|
+
(finding) => finding.scope === "page"
|
|
80
|
+
);
|
|
81
|
+
await kv.set(findingsKey(document.route), {
|
|
82
|
+
route: document.route,
|
|
83
|
+
findings: pageFindings
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
var DEFAULT_COLLECTIONS = ["posts", "pages"];
|
|
87
|
+
async function evaluateAndPersistAll(ctx, options) {
|
|
88
|
+
const collections = options.collections ?? DEFAULT_COLLECTIONS;
|
|
89
|
+
const { kv, log } = ctx;
|
|
90
|
+
const summary = {
|
|
91
|
+
documentsScanned: 0,
|
|
92
|
+
routesUpdated: 0,
|
|
93
|
+
totalFindings: 0,
|
|
94
|
+
errors: []
|
|
95
|
+
};
|
|
96
|
+
const documents = [];
|
|
97
|
+
const documentRoutes = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const collection of collections) {
|
|
99
|
+
let cursor;
|
|
100
|
+
do {
|
|
101
|
+
try {
|
|
102
|
+
const page = await ctx.content.list(collection, {
|
|
103
|
+
limit: 100,
|
|
104
|
+
...cursor === void 0 ? {} : { cursor }
|
|
105
|
+
});
|
|
106
|
+
for (const item of page.items) {
|
|
107
|
+
const document = contentItemToEmdashDocument(item);
|
|
108
|
+
documents.push(document);
|
|
109
|
+
documentRoutes.add(document.route);
|
|
110
|
+
}
|
|
111
|
+
cursor = page.hasMore ? page.cursor : void 0;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
summary.errors.push(`${collection}: ${message}`);
|
|
115
|
+
cursor = void 0;
|
|
116
|
+
}
|
|
117
|
+
} while (cursor !== void 0);
|
|
118
|
+
}
|
|
119
|
+
summary.documentsScanned = documents.length;
|
|
120
|
+
for (const document of documents) {
|
|
121
|
+
await kv.set(documentKey(document.route), document);
|
|
122
|
+
}
|
|
123
|
+
const result = await options.evaluator(documents);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
log?.error?.(`seogeo evaluator failure (${result.reason})`, {
|
|
126
|
+
detail: result.detail
|
|
127
|
+
});
|
|
128
|
+
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
129
|
+
return summary;
|
|
130
|
+
}
|
|
131
|
+
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const route of documentRoutes) {
|
|
133
|
+
findingsByRoute.set(route, []);
|
|
134
|
+
}
|
|
135
|
+
for (const finding of result.findings) {
|
|
136
|
+
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
137
|
+
const list = findingsByRoute.get(bucket) ?? [];
|
|
138
|
+
list.push(finding);
|
|
139
|
+
findingsByRoute.set(bucket, list);
|
|
140
|
+
}
|
|
141
|
+
for (const [route, findings] of findingsByRoute) {
|
|
142
|
+
await kv.set(findingsKey(route), { route, findings });
|
|
143
|
+
summary.routesUpdated += 1;
|
|
144
|
+
}
|
|
145
|
+
summary.totalFindings = result.findings.length;
|
|
146
|
+
return summary;
|
|
147
|
+
}
|
|
148
|
+
function findingsKey(route) {
|
|
149
|
+
const normalized = route === "" || route === "/" ? "/" : route;
|
|
150
|
+
return `findings:${normalized}`;
|
|
151
|
+
}
|
|
152
|
+
function documentKey(route) {
|
|
153
|
+
const normalized = route === "" || route === "/" ? "/" : route;
|
|
154
|
+
return `document:${normalized}`;
|
|
155
|
+
}
|
|
156
|
+
async function readAllDocuments(kv) {
|
|
157
|
+
const entries = await kv.list("document:");
|
|
158
|
+
return entries.map((entry) => entry.value).filter((value) => value !== null);
|
|
159
|
+
}
|
|
160
|
+
async function readFindings(kv, route) {
|
|
161
|
+
const stored = await kv.get(findingsKey(route));
|
|
162
|
+
if (stored === null) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
return stored.findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// wasm/aexeo_emdash_bridge_bg.js
|
|
169
|
+
var aexeo_emdash_bridge_bg_exports = {};
|
|
170
|
+
__export(aexeo_emdash_bridge_bg_exports, {
|
|
171
|
+
__wbg_Error_960c155d3d49e4c2: () => __wbg_Error_960c155d3d49e4c2,
|
|
172
|
+
__wbg_set_wasm: () => __wbg_set_wasm,
|
|
173
|
+
__wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
|
|
174
|
+
evaluateDocuments: () => evaluateDocuments,
|
|
175
|
+
scoreIntelligence: () => scoreIntelligence
|
|
176
|
+
});
|
|
177
|
+
function evaluateDocuments(documents_json, config_json) {
|
|
178
|
+
let deferred4_0;
|
|
179
|
+
let deferred4_1;
|
|
180
|
+
try {
|
|
181
|
+
const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
182
|
+
const len0 = WASM_VECTOR_LEN;
|
|
183
|
+
var ptr1 = isLikeNone(config_json) ? 0 : passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
184
|
+
var len1 = WASM_VECTOR_LEN;
|
|
185
|
+
const ret = wasm.evaluateDocuments(ptr0, len0, ptr1, len1);
|
|
186
|
+
var ptr3 = ret[0];
|
|
187
|
+
var len3 = ret[1];
|
|
188
|
+
if (ret[3]) {
|
|
189
|
+
ptr3 = 0;
|
|
190
|
+
len3 = 0;
|
|
191
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
192
|
+
}
|
|
193
|
+
deferred4_0 = ptr3;
|
|
194
|
+
deferred4_1 = len3;
|
|
195
|
+
return getStringFromWasm0(ptr3, len3);
|
|
196
|
+
} finally {
|
|
197
|
+
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function scoreIntelligence(documents_json) {
|
|
201
|
+
let deferred3_0;
|
|
202
|
+
let deferred3_1;
|
|
203
|
+
try {
|
|
204
|
+
const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
205
|
+
const len0 = WASM_VECTOR_LEN;
|
|
206
|
+
const ret = wasm.scoreIntelligence(ptr0, len0);
|
|
207
|
+
var ptr2 = ret[0];
|
|
208
|
+
var len2 = ret[1];
|
|
209
|
+
if (ret[3]) {
|
|
210
|
+
ptr2 = 0;
|
|
211
|
+
len2 = 0;
|
|
212
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
213
|
+
}
|
|
214
|
+
deferred3_0 = ptr2;
|
|
215
|
+
deferred3_1 = len2;
|
|
216
|
+
return getStringFromWasm0(ptr2, len2);
|
|
217
|
+
} finally {
|
|
218
|
+
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function __wbg_Error_960c155d3d49e4c2(arg0, arg1) {
|
|
222
|
+
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
223
|
+
return ret;
|
|
224
|
+
}
|
|
225
|
+
function __wbindgen_init_externref_table() {
|
|
226
|
+
const table = wasm.__wbindgen_externrefs;
|
|
227
|
+
const offset = table.grow(4);
|
|
228
|
+
table.set(0, void 0);
|
|
229
|
+
table.set(offset + 0, void 0);
|
|
230
|
+
table.set(offset + 1, null);
|
|
231
|
+
table.set(offset + 2, true);
|
|
232
|
+
table.set(offset + 3, false);
|
|
233
|
+
}
|
|
234
|
+
function getStringFromWasm0(ptr, len) {
|
|
235
|
+
ptr = ptr >>> 0;
|
|
236
|
+
return decodeText(ptr, len);
|
|
237
|
+
}
|
|
238
|
+
var cachedUint8ArrayMemory0 = null;
|
|
239
|
+
function getUint8ArrayMemory0() {
|
|
240
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
241
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
242
|
+
}
|
|
243
|
+
return cachedUint8ArrayMemory0;
|
|
244
|
+
}
|
|
245
|
+
function isLikeNone(x) {
|
|
246
|
+
return x === void 0 || x === null;
|
|
247
|
+
}
|
|
248
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
249
|
+
if (realloc === void 0) {
|
|
250
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
251
|
+
const ptr2 = malloc(buf.length, 1) >>> 0;
|
|
252
|
+
getUint8ArrayMemory0().subarray(ptr2, ptr2 + buf.length).set(buf);
|
|
253
|
+
WASM_VECTOR_LEN = buf.length;
|
|
254
|
+
return ptr2;
|
|
255
|
+
}
|
|
256
|
+
let len = arg.length;
|
|
257
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
258
|
+
const mem = getUint8ArrayMemory0();
|
|
259
|
+
let offset = 0;
|
|
260
|
+
for (; offset < len; offset++) {
|
|
261
|
+
const code = arg.charCodeAt(offset);
|
|
262
|
+
if (code > 127) break;
|
|
263
|
+
mem[ptr + offset] = code;
|
|
264
|
+
}
|
|
265
|
+
if (offset !== len) {
|
|
266
|
+
if (offset !== 0) {
|
|
267
|
+
arg = arg.slice(offset);
|
|
268
|
+
}
|
|
269
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
270
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
271
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
272
|
+
offset += ret.written;
|
|
273
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
274
|
+
}
|
|
275
|
+
WASM_VECTOR_LEN = offset;
|
|
276
|
+
return ptr;
|
|
277
|
+
}
|
|
278
|
+
function takeFromExternrefTable0(idx) {
|
|
279
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
280
|
+
wasm.__externref_table_dealloc(idx);
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
var cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
284
|
+
cachedTextDecoder.decode();
|
|
285
|
+
var MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
286
|
+
var numBytesDecoded = 0;
|
|
287
|
+
function decodeText(ptr, len) {
|
|
288
|
+
numBytesDecoded += len;
|
|
289
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
290
|
+
cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
291
|
+
cachedTextDecoder.decode();
|
|
292
|
+
numBytesDecoded = len;
|
|
293
|
+
}
|
|
294
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
295
|
+
}
|
|
296
|
+
var cachedTextEncoder = new TextEncoder();
|
|
297
|
+
if (!("encodeInto" in cachedTextEncoder)) {
|
|
298
|
+
cachedTextEncoder.encodeInto = function(arg, view) {
|
|
299
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
300
|
+
view.set(buf);
|
|
301
|
+
return {
|
|
302
|
+
read: arg.length,
|
|
303
|
+
written: buf.length
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
var WASM_VECTOR_LEN = 0;
|
|
308
|
+
var wasm;
|
|
309
|
+
function __wbg_set_wasm(val) {
|
|
310
|
+
wasm = val;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/wasm-init.ts
|
|
314
|
+
import bridgeModule from "../wasm/aexeo_emdash_bridge_bg.wasm";
|
|
315
|
+
var initialized = false;
|
|
316
|
+
function ensureInitialized() {
|
|
317
|
+
if (initialized) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (!(bridgeModule instanceof WebAssembly.Module)) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"seogeo: WASM module did not resolve to a WebAssembly.Module \u2014 the consumer's bundler isn't configured for .wasm imports. Add the .wasm loader to your Vite/wrangler config; see @aeptus/aexeo-emdash INSTALL.md for the recipe."
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const instance = new WebAssembly.Instance(bridgeModule, {
|
|
326
|
+
"./aexeo_emdash_bridge_bg.js": aexeo_emdash_bridge_bg_exports
|
|
327
|
+
});
|
|
328
|
+
__wbg_set_wasm(instance.exports);
|
|
329
|
+
instance.exports.__wbindgen_start();
|
|
330
|
+
initialized = true;
|
|
331
|
+
}
|
|
332
|
+
async function evaluateDocuments2(documentsJson, configJson) {
|
|
333
|
+
ensureInitialized();
|
|
334
|
+
return evaluateDocuments(documentsJson, configJson);
|
|
335
|
+
}
|
|
336
|
+
async function scoreIntelligence2(documentsJson) {
|
|
337
|
+
ensureInitialized();
|
|
338
|
+
return scoreIntelligence(documentsJson);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/configured.ts
|
|
342
|
+
import { definePlugin } from "emdash";
|
|
343
|
+
var inProcessEvaluator = async (documents) => {
|
|
344
|
+
try {
|
|
345
|
+
const raw = await evaluateDocuments2(JSON.stringify(documents));
|
|
346
|
+
const findings = JSON.parse(raw);
|
|
347
|
+
if (!Array.isArray(findings)) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
reason: "invalid_response",
|
|
351
|
+
detail: "WASM returned non-array body"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { ok: true, findings };
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
357
|
+
return { ok: false, reason: "wasm_error", detail };
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
function createPlugin(_options = {}) {
|
|
361
|
+
return definePlugin({
|
|
362
|
+
id: "aexeo-seogeo",
|
|
363
|
+
version: "0.0.1",
|
|
364
|
+
capabilities: ["read:content"],
|
|
365
|
+
hooks: {
|
|
366
|
+
"content:afterSave": (event, ctx) => handleAfterSaveConfigured(event, ctx, inProcessEvaluator)
|
|
367
|
+
},
|
|
368
|
+
routes: {
|
|
369
|
+
admin: { handler: handleAdminRoute }
|
|
370
|
+
},
|
|
371
|
+
admin: {
|
|
372
|
+
pages: [
|
|
373
|
+
// emdash's /admin/plugins/<id> root navigates to the plugin's
|
|
374
|
+
// "/" page — without it, clicking the plugin in the meta list
|
|
375
|
+
// produces a "Not Found". Alias "/" to the findings page so
|
|
376
|
+
// the entry-point lands somewhere useful.
|
|
377
|
+
{ path: "/", label: "SEO findings" },
|
|
378
|
+
{ path: "/findings", label: "SEO findings" },
|
|
379
|
+
{ path: "/document", label: "Document SEO" }
|
|
380
|
+
],
|
|
381
|
+
widgets: [
|
|
382
|
+
{ id: "seogeo-score", size: "third", title: "SEO score" }
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
var configured_default = createPlugin;
|
|
388
|
+
async function handleAdminRoute(ctx) {
|
|
389
|
+
const body = ctx.input;
|
|
390
|
+
if (body.type === "page_load") {
|
|
391
|
+
return handlePageLoad(ctx, body.page);
|
|
392
|
+
}
|
|
393
|
+
if (body.type === "block_action") {
|
|
394
|
+
if (body.action_id === "view_document" && typeof body.value === "string") {
|
|
395
|
+
return renderDocumentPanel(ctx, body.value);
|
|
396
|
+
}
|
|
397
|
+
if (body.action_id === "refresh_findings") {
|
|
398
|
+
return handleRefresh(ctx);
|
|
399
|
+
}
|
|
400
|
+
if (body.action_id.startsWith("filter:")) {
|
|
401
|
+
return renderFindingsPage(ctx);
|
|
402
|
+
}
|
|
403
|
+
return notFound(body.action_id);
|
|
404
|
+
}
|
|
405
|
+
if (body.type === "form_submit") {
|
|
406
|
+
if (body.action_id === "view_document") {
|
|
407
|
+
const picked = body.values["route_picker"];
|
|
408
|
+
if (typeof picked === "string" && picked.length > 0) {
|
|
409
|
+
return renderDocumentPanel(ctx, picked);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return renderFindingsPage(ctx);
|
|
413
|
+
}
|
|
414
|
+
return renderFindingsPage(ctx);
|
|
415
|
+
}
|
|
416
|
+
async function handlePageLoad(ctx, page) {
|
|
417
|
+
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
418
|
+
if (normalized === "" || normalized === "findings") {
|
|
419
|
+
return renderFindingsPage(ctx);
|
|
420
|
+
}
|
|
421
|
+
if (normalized === "widget:seogeo-score") {
|
|
422
|
+
return renderScoreWidget(ctx);
|
|
423
|
+
}
|
|
424
|
+
if (normalized === "document") {
|
|
425
|
+
return renderDocumentPanel(ctx);
|
|
426
|
+
}
|
|
427
|
+
return notFound(page);
|
|
428
|
+
}
|
|
429
|
+
async function handleRefresh(ctx) {
|
|
430
|
+
let summary;
|
|
431
|
+
try {
|
|
432
|
+
summary = await evaluateAndPersistAll(ctx, {
|
|
433
|
+
evaluator: inProcessEvaluator
|
|
434
|
+
});
|
|
435
|
+
} catch (err) {
|
|
436
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
437
|
+
return {
|
|
438
|
+
blocks: [
|
|
439
|
+
{ type: "header", text: "SEO findings" },
|
|
440
|
+
{
|
|
441
|
+
type: "banner",
|
|
442
|
+
title: `Refresh failed: ${detail}`,
|
|
443
|
+
variant: "error"
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
toast: { message: `Refresh failed: ${detail}`, type: "error" }
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const refreshed = await renderFindingsPage(ctx);
|
|
450
|
+
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`;
|
|
451
|
+
if (summary.errors.length > 0) {
|
|
452
|
+
refreshed.blocks.unshift({
|
|
453
|
+
type: "banner",
|
|
454
|
+
title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
|
|
455
|
+
variant: "alert"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
...refreshed,
|
|
460
|
+
toast: {
|
|
461
|
+
message: toastMessage,
|
|
462
|
+
type: summary.errors.length === 0 ? "success" : "info"
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function readAllFindings(ctx) {
|
|
467
|
+
const entries = await ctx.kv.list(
|
|
468
|
+
"findings:"
|
|
469
|
+
);
|
|
470
|
+
const out = [];
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
if (entry.value === null) continue;
|
|
473
|
+
const route = entry.key.replace(/^findings:/, "");
|
|
474
|
+
for (const finding of entry.value.findings) {
|
|
475
|
+
out.push({ ...finding, document_route: route });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function severityFirst(a, b) {
|
|
481
|
+
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
482
|
+
const diff = rank(a.severity) - rank(b.severity);
|
|
483
|
+
if (diff !== 0) return diff;
|
|
484
|
+
return a.rule_id.localeCompare(b.rule_id);
|
|
485
|
+
}
|
|
486
|
+
function uniqueRoutes(rows) {
|
|
487
|
+
const set = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const row of rows) set.add(row.document_route);
|
|
489
|
+
return [...set].sort();
|
|
490
|
+
}
|
|
491
|
+
async function renderFindingsPage(ctx) {
|
|
492
|
+
const findings = await readAllFindings(ctx);
|
|
493
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
494
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
495
|
+
const sorted = [...findings].sort(severityFirst);
|
|
496
|
+
const routes = uniqueRoutes(findings);
|
|
497
|
+
const blocks = [
|
|
498
|
+
{ type: "header", text: "SEO findings" },
|
|
499
|
+
{
|
|
500
|
+
type: "context",
|
|
501
|
+
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.`
|
|
502
|
+
},
|
|
503
|
+
{ type: "divider" },
|
|
504
|
+
{
|
|
505
|
+
type: "actions",
|
|
506
|
+
elements: [
|
|
507
|
+
{
|
|
508
|
+
type: "button",
|
|
509
|
+
label: "Refresh",
|
|
510
|
+
action_id: "refresh_findings",
|
|
511
|
+
style: "primary"
|
|
512
|
+
},
|
|
513
|
+
{ type: "button", label: "All", action_id: "filter:all" },
|
|
514
|
+
{ type: "button", label: "Errors only", action_id: "filter:errors" },
|
|
515
|
+
{
|
|
516
|
+
type: "button",
|
|
517
|
+
label: "Warnings only",
|
|
518
|
+
action_id: "filter:warnings"
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
sorted.length === 0 ? {
|
|
523
|
+
type: "context",
|
|
524
|
+
text: "Once a Refresh runs, findings will list here."
|
|
525
|
+
} : findingsTable(sorted)
|
|
526
|
+
];
|
|
527
|
+
if (routes.length > 0) {
|
|
528
|
+
blocks.push({
|
|
529
|
+
type: "form",
|
|
530
|
+
fields: [
|
|
531
|
+
{
|
|
532
|
+
type: "select",
|
|
533
|
+
action_id: "route_picker",
|
|
534
|
+
label: "Document to inspect",
|
|
535
|
+
options: routes.map((route) => ({ label: route, value: route }))
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
submit: { label: "View document SEO", action_id: "view_document" }
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return { blocks };
|
|
542
|
+
}
|
|
543
|
+
function findingsTable(rows) {
|
|
544
|
+
return {
|
|
545
|
+
type: "table",
|
|
546
|
+
columns: [
|
|
547
|
+
{ key: "route", label: "Route" },
|
|
548
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
549
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
550
|
+
{ key: "message", label: "Message" }
|
|
551
|
+
],
|
|
552
|
+
rows: rows.map((row) => ({
|
|
553
|
+
route: row.document_route,
|
|
554
|
+
rule: row.rule_id,
|
|
555
|
+
severity: row.severity,
|
|
556
|
+
message: row.message
|
|
557
|
+
}))
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function renderScoreWidget(ctx) {
|
|
561
|
+
const documents = await readAllDocuments(ctx.kv);
|
|
562
|
+
if (documents.length === 0) {
|
|
563
|
+
return {
|
|
564
|
+
blocks: [
|
|
565
|
+
{ type: "header", text: "SEO score" },
|
|
566
|
+
{
|
|
567
|
+
type: "context",
|
|
568
|
+
text: "No documents indexed \u2014 click Refresh on the seogeo findings page."
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const score = await scoreLocally(documents);
|
|
574
|
+
const blocks = [
|
|
575
|
+
{
|
|
576
|
+
type: "stats",
|
|
577
|
+
items: [
|
|
578
|
+
{ label: "Overall", value: `${score.overall_score}` },
|
|
579
|
+
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
580
|
+
{ label: "Truth", value: `${score.truth_consistency_score}` },
|
|
581
|
+
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
];
|
|
585
|
+
if (score.overall_score < 60) {
|
|
586
|
+
blocks.unshift({
|
|
587
|
+
type: "banner",
|
|
588
|
+
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
589
|
+
variant: "alert"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (score.blockers.length > 0) {
|
|
593
|
+
blocks.push({ type: "context", text: topBlockersLine(score) });
|
|
594
|
+
}
|
|
595
|
+
return { blocks };
|
|
596
|
+
}
|
|
597
|
+
async function scoreLocally(documents) {
|
|
598
|
+
const raw = await scoreIntelligence2(JSON.stringify(documents));
|
|
599
|
+
return JSON.parse(raw);
|
|
600
|
+
}
|
|
601
|
+
function topBlockersLine(score) {
|
|
602
|
+
const top = score.blockers.slice(0, 3).map((b) => b.message);
|
|
603
|
+
if (top.length === 0) return "No blockers identified.";
|
|
604
|
+
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
605
|
+
}
|
|
606
|
+
async function renderDocumentPanel(ctx, route) {
|
|
607
|
+
if (route === void 0) {
|
|
608
|
+
return {
|
|
609
|
+
blocks: [
|
|
610
|
+
{ type: "header", text: "Document SEO" },
|
|
611
|
+
{
|
|
612
|
+
type: "context",
|
|
613
|
+
text: "Pick a document from the SEO findings list to see its rule findings here."
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const findings = await readFindings(ctx.kv, route);
|
|
619
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
620
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
621
|
+
const sorted = [...findings].sort(severityFirst);
|
|
622
|
+
return {
|
|
623
|
+
blocks: [
|
|
624
|
+
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
625
|
+
{
|
|
626
|
+
type: "context",
|
|
627
|
+
text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
628
|
+
},
|
|
629
|
+
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
630
|
+
]
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function documentFindingsTable(findings) {
|
|
634
|
+
return {
|
|
635
|
+
type: "table",
|
|
636
|
+
columns: [
|
|
637
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
638
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
639
|
+
{ key: "message", label: "Message" },
|
|
640
|
+
{ key: "block", label: "Block", format: "code" }
|
|
641
|
+
],
|
|
642
|
+
rows: findings.map((finding) => ({
|
|
643
|
+
rule: finding.rule_id,
|
|
644
|
+
severity: finding.severity,
|
|
645
|
+
message: finding.message,
|
|
646
|
+
block: `${finding.path}:${finding.line}`
|
|
647
|
+
}))
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function notFound(page) {
|
|
651
|
+
return {
|
|
652
|
+
blocks: [
|
|
653
|
+
{ type: "header", text: "Not found" },
|
|
654
|
+
{ type: "context", text: `unknown page: ${page}` }
|
|
655
|
+
]
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
export {
|
|
659
|
+
createPlugin,
|
|
660
|
+
configured_default as default
|
|
661
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EmdashDocument, Finding, SiteIntelligenceScore } from "./types.js";
|
|
2
|
+
export declare function evaluate(_documents: readonly EmdashDocument[]): Promise<Finding[]>;
|
|
3
|
+
export declare function scoreSite(_documents: readonly EmdashDocument[]): Promise<SiteIntelligenceScore>;
|
|
4
|
+
export declare function errorFindings(findings: readonly Finding[]): Finding[];
|
|
5
|
+
export declare function findingsByRoute(findings: readonly Finding[]): Map<string, Finding[]>;
|
|
6
|
+
//# sourceMappingURL=evaluator-sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator-sandbox.d.ts","sourceRoot":"","sources":["../src/evaluator-sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAkBpB,wBAAsB,QAAQ,CAC5B,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,OAAO,CAAC,OAAO,EAAE,CAAC,CAEpB;AAED,wBAAsB,SAAS,CAC7B,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,OAAO,CAAC,qBAAqB,CAAC,CAUhC;AAQD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAErE;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,OAAO,EAAE,GAC3B,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAWxB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmdashDocument, Finding, SiteIntelligenceScore } from "./types.js";
|
|
2
|
+
export interface EvaluateOptions {
|
|
3
|
+
configJson?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function evaluate(documents: readonly EmdashDocument[], options?: EvaluateOptions): Promise<Finding[]>;
|
|
6
|
+
export declare function errorFindings(findings: readonly Finding[]): Finding[];
|
|
7
|
+
export declare function findingsByRoute(findings: readonly Finding[]): Map<string, Finding[]>;
|
|
8
|
+
export declare function scoreSite(documents: readonly EmdashDocument[]): Promise<SiteIntelligenceScore>;
|
|
9
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../src/evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAapB,MAAM,WAAW,eAAe;IAG9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,EAAE,CAAC,CAIpB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAErE;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,OAAO,EAAE,GAC3B,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAWxB;AAKD,wBAAsB,SAAS,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,GACnC,OAAO,CAAC,qBAAqB,CAAC,CAGhC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { seogeoPluginSandboxed } from "./sandbox.js";
|
|
2
|
+
export type { PluginAdminPage, PluginAdminWidget, SandboxedPluginDescriptor, SeogeoSandboxedOptions, } from "./sandbox.js";
|
|
3
|
+
export interface ConfiguredPluginDescriptor {
|
|
4
|
+
id: string;
|
|
5
|
+
version: string;
|
|
6
|
+
entrypoint: string;
|
|
7
|
+
options: Record<string, unknown>;
|
|
8
|
+
capabilities: readonly string[];
|
|
9
|
+
allowedHosts?: readonly string[];
|
|
10
|
+
adminPages: readonly {
|
|
11
|
+
path: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}[];
|
|
14
|
+
adminWidgets: readonly {
|
|
15
|
+
id: string;
|
|
16
|
+
size?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
export interface SeogeoPluginOptions {
|
|
21
|
+
}
|
|
22
|
+
export declare function seogeoPlugin(options?: SeogeoPluginOptions): ConfiguredPluginDescriptor;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|