@rankmyseo/server 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/LICENSE +201 -0
- package/dist/index.cjs +571 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +563 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LanguageModel } from 'ai';
|
|
2
|
+
import { TenantScope, RankMySeoConfig, RankStore } from '@rankmyseo/core';
|
|
3
|
+
|
|
4
|
+
interface RequestScope extends TenantScope {
|
|
5
|
+
}
|
|
6
|
+
declare function readScope(request: Request): RequestScope | Response;
|
|
7
|
+
|
|
8
|
+
interface HandlerOptions {
|
|
9
|
+
config?: RankMySeoConfig;
|
|
10
|
+
agentModel?: LanguageModel;
|
|
11
|
+
}
|
|
12
|
+
declare function createHandler(store: RankStore, options?: HandlerOptions): (request: Request) => Promise<Response>;
|
|
13
|
+
|
|
14
|
+
export { type HandlerOptions, type RequestScope, createHandler, readScope };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// src/handler.ts
|
|
2
|
+
import "server-only";
|
|
3
|
+
import {
|
|
4
|
+
defineConfig
|
|
5
|
+
} from "@rankmyseo/core";
|
|
6
|
+
|
|
7
|
+
// src/routes.ts
|
|
8
|
+
import "server-only";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
import {
|
|
11
|
+
buildAuditRecommendations,
|
|
12
|
+
buildBlogRecommendations,
|
|
13
|
+
buildReport,
|
|
14
|
+
createAuditInputSchema,
|
|
15
|
+
createBlogPostInputSchema,
|
|
16
|
+
createKeywordInputSchema,
|
|
17
|
+
createRankSnapshotInputSchema,
|
|
18
|
+
dashboardConfigSchema,
|
|
19
|
+
extractPageSignals,
|
|
20
|
+
generateMeta,
|
|
21
|
+
normalizeHttpUrl,
|
|
22
|
+
pageSignalsSchema,
|
|
23
|
+
projectSchema,
|
|
24
|
+
runAuditChecks,
|
|
25
|
+
slugify,
|
|
26
|
+
snapshotRangeQuerySchema,
|
|
27
|
+
updateBlogPostInputSchema
|
|
28
|
+
} from "@rankmyseo/core";
|
|
29
|
+
import { streamAgentChat } from "@rankmyseo/agent";
|
|
30
|
+
|
|
31
|
+
// src/utils.ts
|
|
32
|
+
function readScope(request) {
|
|
33
|
+
const tenantId = request.headers.get("x-tenant-id");
|
|
34
|
+
const projectId = request.headers.get("x-project-id");
|
|
35
|
+
if (!tenantId || !projectId) {
|
|
36
|
+
return Response.json(
|
|
37
|
+
{ error: "Missing or invalid x-tenant-id / x-project-id headers" },
|
|
38
|
+
{ status: 400 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return { tenantId, projectId };
|
|
42
|
+
}
|
|
43
|
+
async function readJson(request) {
|
|
44
|
+
try {
|
|
45
|
+
return await request.json();
|
|
46
|
+
} catch {
|
|
47
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function acceptsMarkdown(request) {
|
|
51
|
+
const accept = request.headers.get("accept") ?? "";
|
|
52
|
+
return accept.includes("text/markdown");
|
|
53
|
+
}
|
|
54
|
+
function buildSitemapXml(routes2, baseUrl) {
|
|
55
|
+
const urls = routes2.map(
|
|
56
|
+
(route) => ` <url><loc>${baseUrl}${route === "/" ? "" : route}</loc></url>`
|
|
57
|
+
).join("\n");
|
|
58
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
59
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
60
|
+
${urls}
|
|
61
|
+
</urlset>`;
|
|
62
|
+
}
|
|
63
|
+
function buildLlmsTxt(config) {
|
|
64
|
+
const name = config.llmsTxt?.projectName ?? "RankMySEO Project";
|
|
65
|
+
const summary = config.llmsTxt?.summary ?? "SEO tracking, audits, and rank history for this site.";
|
|
66
|
+
const links = config.llmsTxt?.links ?? [
|
|
67
|
+
{ title: "Documentation", url: "/docs.md" }
|
|
68
|
+
];
|
|
69
|
+
const linkBlock = links.map((l) => `- [${l.title}](${l.url})`).join("\n");
|
|
70
|
+
return `# ${name}
|
|
71
|
+
|
|
72
|
+
> ${summary}
|
|
73
|
+
|
|
74
|
+
## Resources
|
|
75
|
+
|
|
76
|
+
${linkBlock}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function pageToMarkdown(pathname, title) {
|
|
80
|
+
return `# ${title}
|
|
81
|
+
|
|
82
|
+
Path: \`${pathname}\`
|
|
83
|
+
|
|
84
|
+
This page is available as Markdown for AI agents.
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
function withMarkdownNegotiation(request, html, markdown, pathname) {
|
|
88
|
+
const wantsMarkdown = acceptsMarkdown(request);
|
|
89
|
+
const accept = request.headers.get("accept") ?? "";
|
|
90
|
+
if (wantsMarkdown) {
|
|
91
|
+
return new Response(markdown, {
|
|
92
|
+
status: 200,
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
95
|
+
Vary: "Accept",
|
|
96
|
+
Link: `<${pathname}?format=html>; rel="alternate"; type="text/html"`
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (accept && !accept.includes("*/*") && !accept.includes("text/html")) {
|
|
101
|
+
return Response.json(
|
|
102
|
+
{ error: "Not acceptable", supported: ["text/html", "text/markdown"] },
|
|
103
|
+
{ status: 406, headers: { Vary: "Accept" } }
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return new Response(html, {
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
110
|
+
Vary: "Accept",
|
|
111
|
+
Link: `<${pathname}>; rel="alternate"; type="text/markdown"`
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/routes.ts
|
|
117
|
+
var routes = [];
|
|
118
|
+
function addRoute(method, pattern, handler) {
|
|
119
|
+
routes.push({ method, pattern, handler });
|
|
120
|
+
}
|
|
121
|
+
addRoute("GET", /^\/projects$/, async (_req, ctx) => {
|
|
122
|
+
const data = await ctx.store.projects.list(ctx.scope);
|
|
123
|
+
return Response.json({ data });
|
|
124
|
+
});
|
|
125
|
+
addRoute("POST", /^\/projects$/, async (request, ctx) => {
|
|
126
|
+
const body = await readJson(request);
|
|
127
|
+
if (body instanceof Response) return body;
|
|
128
|
+
const parsed = projectSchema.omit({ createdAt: true, updatedAt: true }).safeParse(body);
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return Response.json({ error: "Invalid project", details: parsed.error.flatten() }, { status: 400 });
|
|
131
|
+
}
|
|
132
|
+
const project = await ctx.store.projects.create({
|
|
133
|
+
...parsed.data,
|
|
134
|
+
tenantId: ctx.scope.tenantId
|
|
135
|
+
});
|
|
136
|
+
return Response.json({ data: project }, { status: 201 });
|
|
137
|
+
});
|
|
138
|
+
addRoute("GET", /^\/projects\/([^/]+)$/, async (_req, ctx, params) => {
|
|
139
|
+
const project = await ctx.store.projects.getById(ctx.scope, params[1]);
|
|
140
|
+
if (!project) return Response.json({ error: "Not found" }, { status: 404 });
|
|
141
|
+
return Response.json({ data: project });
|
|
142
|
+
});
|
|
143
|
+
addRoute("GET", /^\/keywords$/, async (_req, ctx) => {
|
|
144
|
+
const data = await ctx.store.keywords.list(ctx.scope);
|
|
145
|
+
return Response.json({ data });
|
|
146
|
+
});
|
|
147
|
+
addRoute("POST", /^\/keywords$/, async (request, ctx) => {
|
|
148
|
+
const body = await readJson(request);
|
|
149
|
+
if (body instanceof Response) return body;
|
|
150
|
+
const parsed = createKeywordInputSchema.safeParse({
|
|
151
|
+
...body,
|
|
152
|
+
tenantId: ctx.scope.tenantId,
|
|
153
|
+
projectId: ctx.scope.projectId
|
|
154
|
+
});
|
|
155
|
+
if (!parsed.success) {
|
|
156
|
+
return Response.json({ error: "Invalid keyword", details: parsed.error.flatten() }, { status: 400 });
|
|
157
|
+
}
|
|
158
|
+
const keyword = await ctx.store.keywords.create(parsed.data);
|
|
159
|
+
return Response.json({ data: keyword }, { status: 201 });
|
|
160
|
+
});
|
|
161
|
+
addRoute("GET", /^\/keywords\/([^/]+)$/, async (_req, ctx, params) => {
|
|
162
|
+
const keyword = await ctx.store.keywords.getById(ctx.scope, params[1]);
|
|
163
|
+
if (!keyword) return Response.json({ error: "Not found" }, { status: 404 });
|
|
164
|
+
return Response.json({ data: keyword });
|
|
165
|
+
});
|
|
166
|
+
addRoute("DELETE", /^\/keywords\/([^/]+)$/, async (_req, ctx, params) => {
|
|
167
|
+
const deleted = await ctx.store.keywords.delete(ctx.scope, params[1]);
|
|
168
|
+
if (!deleted) return Response.json({ error: "Not found" }, { status: 404 });
|
|
169
|
+
return new Response(null, { status: 204 });
|
|
170
|
+
});
|
|
171
|
+
addRoute("POST", /^\/snapshots$/, async (request, ctx) => {
|
|
172
|
+
const body = await readJson(request);
|
|
173
|
+
if (body instanceof Response) return body;
|
|
174
|
+
const parsed = createRankSnapshotInputSchema.safeParse({
|
|
175
|
+
...body,
|
|
176
|
+
tenantId: ctx.scope.tenantId,
|
|
177
|
+
projectId: ctx.scope.projectId
|
|
178
|
+
});
|
|
179
|
+
if (!parsed.success) {
|
|
180
|
+
return Response.json({ error: "Invalid snapshot", details: parsed.error.flatten() }, { status: 400 });
|
|
181
|
+
}
|
|
182
|
+
const snapshot = await ctx.store.snapshots.append(parsed.data);
|
|
183
|
+
return Response.json({ data: snapshot }, { status: 201 });
|
|
184
|
+
});
|
|
185
|
+
addRoute("GET", /^\/snapshots$/, async (_req, ctx, _params, url) => {
|
|
186
|
+
const parsed = snapshotRangeQuerySchema.safeParse({
|
|
187
|
+
tenantId: ctx.scope.tenantId,
|
|
188
|
+
projectId: ctx.scope.projectId,
|
|
189
|
+
keywordId: url.searchParams.get("keywordId") ?? void 0,
|
|
190
|
+
from: url.searchParams.get("from"),
|
|
191
|
+
to: url.searchParams.get("to")
|
|
192
|
+
});
|
|
193
|
+
if (!parsed.success) {
|
|
194
|
+
return Response.json({ error: "Invalid query", details: parsed.error.flatten() }, { status: 400 });
|
|
195
|
+
}
|
|
196
|
+
const data = await ctx.store.snapshots.listByRange(parsed.data);
|
|
197
|
+
return Response.json({ data });
|
|
198
|
+
});
|
|
199
|
+
addRoute("GET", /^\/audits$/, async (_req, ctx) => {
|
|
200
|
+
const data = await ctx.store.audits.list(ctx.scope);
|
|
201
|
+
return Response.json({ data });
|
|
202
|
+
});
|
|
203
|
+
addRoute("POST", /^\/audits$/, async (request, ctx) => {
|
|
204
|
+
const body = await readJson(request);
|
|
205
|
+
if (body instanceof Response) return body;
|
|
206
|
+
const parsed = createAuditInputSchema.safeParse({
|
|
207
|
+
...body,
|
|
208
|
+
tenantId: ctx.scope.tenantId,
|
|
209
|
+
projectId: ctx.scope.projectId
|
|
210
|
+
});
|
|
211
|
+
if (!parsed.success) {
|
|
212
|
+
return Response.json({ error: "Invalid audit", details: parsed.error.flatten() }, { status: 400 });
|
|
213
|
+
}
|
|
214
|
+
const audit = await ctx.store.audits.create({
|
|
215
|
+
...parsed.data,
|
|
216
|
+
id: randomUUID()
|
|
217
|
+
});
|
|
218
|
+
return Response.json({ data: audit }, { status: 201 });
|
|
219
|
+
});
|
|
220
|
+
addRoute("GET", /^\/audits\/([^/]+)$/, async (_req, ctx, params) => {
|
|
221
|
+
const audit = await ctx.store.audits.getById(ctx.scope, params[1]);
|
|
222
|
+
if (!audit) return Response.json({ error: "Not found" }, { status: 404 });
|
|
223
|
+
return Response.json({ data: audit });
|
|
224
|
+
});
|
|
225
|
+
addRoute("POST", /^\/collect$/, async (request, ctx) => {
|
|
226
|
+
if (!ctx.config.siteFeatures.collector) {
|
|
227
|
+
return Response.json({ error: "Collector disabled" }, { status: 403 });
|
|
228
|
+
}
|
|
229
|
+
const body = await readJson(request);
|
|
230
|
+
if (body instanceof Response) return body;
|
|
231
|
+
const parsed = pageSignalsSchema.safeParse(body);
|
|
232
|
+
if (!parsed.success) {
|
|
233
|
+
return Response.json({ error: "Invalid signals", details: parsed.error.flatten() }, { status: 400 });
|
|
234
|
+
}
|
|
235
|
+
const { checks, score } = runAuditChecks(parsed.data);
|
|
236
|
+
const audit = await ctx.store.audits.create({
|
|
237
|
+
id: randomUUID(),
|
|
238
|
+
tenantId: ctx.scope.tenantId,
|
|
239
|
+
projectId: ctx.scope.projectId,
|
|
240
|
+
url: parsed.data.url,
|
|
241
|
+
score,
|
|
242
|
+
checks
|
|
243
|
+
});
|
|
244
|
+
return Response.json({ data: audit }, { status: 201 });
|
|
245
|
+
});
|
|
246
|
+
addRoute("POST", /^\/scan$/, async (request, ctx) => {
|
|
247
|
+
const body = await readJson(request);
|
|
248
|
+
if (body instanceof Response) return body;
|
|
249
|
+
let target;
|
|
250
|
+
try {
|
|
251
|
+
target = normalizeHttpUrl(String(body.url ?? ""));
|
|
252
|
+
} catch {
|
|
253
|
+
return Response.json({ error: "A valid url is required" }, { status: 400 });
|
|
254
|
+
}
|
|
255
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
256
|
+
return Response.json({ error: "Only http(s) URLs can be scanned" }, { status: 400 });
|
|
257
|
+
}
|
|
258
|
+
let html;
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(target.toString(), {
|
|
261
|
+
headers: { "user-agent": "RankMySEO-Scanner/1.0" },
|
|
262
|
+
redirect: "follow"
|
|
263
|
+
});
|
|
264
|
+
if (!res.ok) {
|
|
265
|
+
return Response.json(
|
|
266
|
+
{ error: `Fetch failed with status ${res.status}` },
|
|
267
|
+
{ status: 502 }
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
html = await res.text();
|
|
271
|
+
} catch {
|
|
272
|
+
return Response.json({ error: "Could not fetch the target URL" }, { status: 502 });
|
|
273
|
+
}
|
|
274
|
+
const signals = extractPageSignals(html, target.toString());
|
|
275
|
+
const { checks, score } = runAuditChecks(signals);
|
|
276
|
+
const audit = await ctx.store.audits.create({
|
|
277
|
+
id: randomUUID(),
|
|
278
|
+
tenantId: ctx.scope.tenantId,
|
|
279
|
+
projectId: ctx.scope.projectId,
|
|
280
|
+
url: target.toString(),
|
|
281
|
+
score,
|
|
282
|
+
checks
|
|
283
|
+
});
|
|
284
|
+
const recommendations = buildAuditRecommendations(checks);
|
|
285
|
+
return Response.json(
|
|
286
|
+
{ data: { audit, signals, recommendations } },
|
|
287
|
+
{ status: 201 }
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
addRoute("POST", /^\/meta\/generate$/, async (request, _ctx) => {
|
|
291
|
+
const body = await readJson(request);
|
|
292
|
+
if (body instanceof Response) return body;
|
|
293
|
+
if (!body.title || !body.title.trim()) {
|
|
294
|
+
return Response.json({ error: "title is required" }, { status: 400 });
|
|
295
|
+
}
|
|
296
|
+
const meta = generateMeta({
|
|
297
|
+
title: body.title,
|
|
298
|
+
content: body.content,
|
|
299
|
+
targetKeyword: body.targetKeyword,
|
|
300
|
+
url: body.url,
|
|
301
|
+
siteName: body.siteName
|
|
302
|
+
});
|
|
303
|
+
const { checks, score } = runAuditChecks({
|
|
304
|
+
url: body.url && /^https?:\/\//.test(body.url) ? body.url : "https://example.com",
|
|
305
|
+
title: meta.metaTitle,
|
|
306
|
+
metaDescription: meta.metaDescription,
|
|
307
|
+
canonical: meta.canonical && /^https?:\/\//.test(meta.canonical) ? meta.canonical : null,
|
|
308
|
+
h1Count: 1,
|
|
309
|
+
hasOgTags: true,
|
|
310
|
+
hasJsonLd: true
|
|
311
|
+
});
|
|
312
|
+
return Response.json({ data: { meta, checks, score } });
|
|
313
|
+
});
|
|
314
|
+
function blogDisabled(ctx) {
|
|
315
|
+
if (!ctx.config.siteFeatures.blog) {
|
|
316
|
+
return Response.json({ error: "Blog module disabled" }, { status: 403 });
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
addRoute("GET", /^\/blog$/, async (_req, ctx) => {
|
|
321
|
+
const denied = blogDisabled(ctx);
|
|
322
|
+
if (denied) return denied;
|
|
323
|
+
const data = await ctx.store.blog.list(ctx.scope);
|
|
324
|
+
return Response.json({ data });
|
|
325
|
+
});
|
|
326
|
+
addRoute("POST", /^\/blog$/, async (request, ctx) => {
|
|
327
|
+
const denied = blogDisabled(ctx);
|
|
328
|
+
if (denied) return denied;
|
|
329
|
+
const body = await readJson(request);
|
|
330
|
+
if (body instanceof Response) return body;
|
|
331
|
+
const parsed = createBlogPostInputSchema.safeParse({
|
|
332
|
+
...body,
|
|
333
|
+
tenantId: ctx.scope.tenantId,
|
|
334
|
+
projectId: ctx.scope.projectId
|
|
335
|
+
});
|
|
336
|
+
if (!parsed.success) {
|
|
337
|
+
return Response.json({ error: "Invalid blog post", details: parsed.error.flatten() }, { status: 400 });
|
|
338
|
+
}
|
|
339
|
+
const input = parsed.data;
|
|
340
|
+
const slug = input.slug?.trim() || slugify(input.title);
|
|
341
|
+
let { metaTitle, metaDescription } = input;
|
|
342
|
+
if (!metaTitle.trim() || !metaDescription.trim()) {
|
|
343
|
+
const meta = generateMeta({
|
|
344
|
+
title: input.title,
|
|
345
|
+
content: input.content,
|
|
346
|
+
targetKeyword: input.targetKeyword
|
|
347
|
+
});
|
|
348
|
+
metaTitle = metaTitle.trim() || meta.metaTitle;
|
|
349
|
+
metaDescription = metaDescription.trim() || meta.metaDescription;
|
|
350
|
+
}
|
|
351
|
+
const post = await ctx.store.blog.create({
|
|
352
|
+
id: randomUUID(),
|
|
353
|
+
tenantId: ctx.scope.tenantId,
|
|
354
|
+
projectId: ctx.scope.projectId,
|
|
355
|
+
title: input.title,
|
|
356
|
+
slug,
|
|
357
|
+
content: input.content,
|
|
358
|
+
targetKeyword: input.targetKeyword,
|
|
359
|
+
intent: input.intent,
|
|
360
|
+
metaTitle,
|
|
361
|
+
metaDescription,
|
|
362
|
+
status: input.status
|
|
363
|
+
});
|
|
364
|
+
return Response.json({ data: post }, { status: 201 });
|
|
365
|
+
});
|
|
366
|
+
addRoute("GET", /^\/blog\/([^/]+)$/, async (_req, ctx, params) => {
|
|
367
|
+
const denied = blogDisabled(ctx);
|
|
368
|
+
if (denied) return denied;
|
|
369
|
+
const post = await ctx.store.blog.getById(ctx.scope, params[1]);
|
|
370
|
+
if (!post) return Response.json({ error: "Not found" }, { status: 404 });
|
|
371
|
+
const recommendations = buildBlogRecommendations({
|
|
372
|
+
intent: post.intent,
|
|
373
|
+
targetKeyword: post.targetKeyword,
|
|
374
|
+
metaTitle: post.metaTitle,
|
|
375
|
+
metaDescription: post.metaDescription,
|
|
376
|
+
content: post.content
|
|
377
|
+
});
|
|
378
|
+
return Response.json({ data: post, recommendations });
|
|
379
|
+
});
|
|
380
|
+
addRoute("PUT", /^\/blog\/([^/]+)$/, async (request, ctx, params) => {
|
|
381
|
+
const denied = blogDisabled(ctx);
|
|
382
|
+
if (denied) return denied;
|
|
383
|
+
const body = await readJson(request);
|
|
384
|
+
if (body instanceof Response) return body;
|
|
385
|
+
const parsed = updateBlogPostInputSchema.safeParse(body);
|
|
386
|
+
if (!parsed.success) {
|
|
387
|
+
return Response.json({ error: "Invalid blog post", details: parsed.error.flatten() }, { status: 400 });
|
|
388
|
+
}
|
|
389
|
+
const updated = await ctx.store.blog.update(ctx.scope, params[1], parsed.data);
|
|
390
|
+
if (!updated) return Response.json({ error: "Not found" }, { status: 404 });
|
|
391
|
+
return Response.json({ data: updated });
|
|
392
|
+
});
|
|
393
|
+
addRoute("DELETE", /^\/blog\/([^/]+)$/, async (_req, ctx, params) => {
|
|
394
|
+
const denied = blogDisabled(ctx);
|
|
395
|
+
if (denied) return denied;
|
|
396
|
+
const deleted = await ctx.store.blog.delete(ctx.scope, params[1]);
|
|
397
|
+
if (!deleted) return Response.json({ error: "Not found" }, { status: 404 });
|
|
398
|
+
return new Response(null, { status: 204 });
|
|
399
|
+
});
|
|
400
|
+
addRoute("GET", /^\/reports$/, async (_req, ctx) => {
|
|
401
|
+
const data = await ctx.store.reports.list(ctx.scope);
|
|
402
|
+
return Response.json({ data });
|
|
403
|
+
});
|
|
404
|
+
addRoute("POST", /^\/reports$/, async (request, ctx) => {
|
|
405
|
+
const body = await readJson(request);
|
|
406
|
+
if (body instanceof Response) return body;
|
|
407
|
+
const from = new Date(String(body.from));
|
|
408
|
+
const to = new Date(String(body.to));
|
|
409
|
+
const title = String(body.title ?? "Report");
|
|
410
|
+
const keywords = await ctx.store.keywords.list(ctx.scope);
|
|
411
|
+
const snapshots = await ctx.store.snapshots.listByRange({
|
|
412
|
+
tenantId: ctx.scope.tenantId,
|
|
413
|
+
projectId: ctx.scope.projectId,
|
|
414
|
+
from,
|
|
415
|
+
to
|
|
416
|
+
});
|
|
417
|
+
const audits = await ctx.store.audits.list(ctx.scope);
|
|
418
|
+
const reportData = buildReport({
|
|
419
|
+
tenantId: ctx.scope.tenantId,
|
|
420
|
+
projectId: ctx.scope.projectId,
|
|
421
|
+
title,
|
|
422
|
+
from,
|
|
423
|
+
to,
|
|
424
|
+
keywords,
|
|
425
|
+
snapshots,
|
|
426
|
+
audits
|
|
427
|
+
});
|
|
428
|
+
const report = await ctx.store.reports.create(reportData);
|
|
429
|
+
return Response.json({ data: report }, { status: 201 });
|
|
430
|
+
});
|
|
431
|
+
addRoute("GET", /^\/reports\/([^/]+)$/, async (_req, ctx, params) => {
|
|
432
|
+
const report = await ctx.store.reports.getById(ctx.scope, params[1]);
|
|
433
|
+
if (!report) return Response.json({ error: "Not found" }, { status: 404 });
|
|
434
|
+
return Response.json({ data: report });
|
|
435
|
+
});
|
|
436
|
+
addRoute("GET", /^\/dashboard$/, async (_req, ctx) => {
|
|
437
|
+
const config = await ctx.store.dashboard.get(ctx.scope);
|
|
438
|
+
return Response.json({ data: config ?? null });
|
|
439
|
+
});
|
|
440
|
+
addRoute("PUT", /^\/dashboard$/, async (request, ctx) => {
|
|
441
|
+
const body = await readJson(request);
|
|
442
|
+
if (body instanceof Response) return body;
|
|
443
|
+
const existing = await ctx.store.dashboard.get(ctx.scope);
|
|
444
|
+
const parsed = dashboardConfigSchema.safeParse({
|
|
445
|
+
...body,
|
|
446
|
+
id: existing?.id ?? randomUUID(),
|
|
447
|
+
tenantId: ctx.scope.tenantId,
|
|
448
|
+
projectId: ctx.scope.projectId,
|
|
449
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
450
|
+
});
|
|
451
|
+
if (!parsed.success) {
|
|
452
|
+
return Response.json({ error: "Invalid dashboard", details: parsed.error.flatten() }, { status: 400 });
|
|
453
|
+
}
|
|
454
|
+
const saved = await ctx.store.dashboard.upsert(parsed.data);
|
|
455
|
+
return Response.json({ data: saved });
|
|
456
|
+
});
|
|
457
|
+
addRoute("POST", /^\/agent\/chat$/, async (request, ctx) => {
|
|
458
|
+
if (!ctx.agentModel) {
|
|
459
|
+
return Response.json({ error: "Agent model not configured" }, { status: 503 });
|
|
460
|
+
}
|
|
461
|
+
const body = await readJson(request);
|
|
462
|
+
if (body instanceof Response) return body;
|
|
463
|
+
const result = await streamAgentChat({
|
|
464
|
+
store: ctx.store,
|
|
465
|
+
scope: ctx.scope,
|
|
466
|
+
model: ctx.agentModel,
|
|
467
|
+
messages: body.messages ?? []
|
|
468
|
+
});
|
|
469
|
+
return result.toTextStreamResponse();
|
|
470
|
+
});
|
|
471
|
+
addRoute("GET", /^\/sitemap\.xml$/, async (_req, ctx) => {
|
|
472
|
+
if (!ctx.config.siteFeatures.sitemap) {
|
|
473
|
+
return Response.json({ error: "Sitemap disabled" }, { status: 404 });
|
|
474
|
+
}
|
|
475
|
+
const projects = await ctx.store.projects.list(ctx.scope);
|
|
476
|
+
const domain = projects[0]?.domain ?? "example.com";
|
|
477
|
+
const baseUrl = domain.startsWith("http") ? domain : `https://${domain}`;
|
|
478
|
+
const xml = buildSitemapXml(ctx.config.sitemapRoutes, baseUrl);
|
|
479
|
+
return new Response(xml, {
|
|
480
|
+
headers: { "Content-Type": "application/xml; charset=utf-8" }
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
addRoute("GET", /^\/llms\.txt$/, async (_req, ctx) => {
|
|
484
|
+
if (!ctx.config.siteFeatures.llmsTxt) {
|
|
485
|
+
return Response.json({ error: "llms.txt disabled" }, { status: 404 });
|
|
486
|
+
}
|
|
487
|
+
const text = buildLlmsTxt(ctx.config);
|
|
488
|
+
return new Response(text, {
|
|
489
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" }
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
addRoute("GET", /^\/$/, async (request, ctx, _params, url) => {
|
|
493
|
+
if (!ctx.config.siteFeatures.markdownNegotiation) {
|
|
494
|
+
return Response.json({ ok: true, service: "rankmyseo" });
|
|
495
|
+
}
|
|
496
|
+
const html = `<!DOCTYPE html><html><head><title>RankMySEO</title></head><body><h1>RankMySEO</h1><p>SEO toolkit API</p></body></html>`;
|
|
497
|
+
const md = pageToMarkdown(url.pathname, "RankMySEO");
|
|
498
|
+
return withMarkdownNegotiation(request, html, md, url.pathname);
|
|
499
|
+
});
|
|
500
|
+
async function dispatchRoute(request, ctx) {
|
|
501
|
+
const url = new URL(request.url);
|
|
502
|
+
const method = request.method.toUpperCase();
|
|
503
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
504
|
+
for (const route of routes) {
|
|
505
|
+
if (route.method !== method) continue;
|
|
506
|
+
const match = pathname.match(route.pattern);
|
|
507
|
+
if (!match) continue;
|
|
508
|
+
const params = {};
|
|
509
|
+
match.slice(1).forEach((value, index) => {
|
|
510
|
+
params[String(index + 1)] = value;
|
|
511
|
+
});
|
|
512
|
+
return route.handler(request, ctx, params, url);
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/handler.ts
|
|
518
|
+
var defaultConfig = defineConfig({
|
|
519
|
+
databaseUrl: "sqlite://:memory:",
|
|
520
|
+
tenantId: "default",
|
|
521
|
+
projectId: "default",
|
|
522
|
+
dataSources: [{ provider: "fixture", default: true }],
|
|
523
|
+
schedule: { cron: "0 6 * * *", enabled: false },
|
|
524
|
+
siteFeatures: {
|
|
525
|
+
sitemap: true,
|
|
526
|
+
llmsTxt: true,
|
|
527
|
+
collector: true,
|
|
528
|
+
markdownNegotiation: true,
|
|
529
|
+
blog: false
|
|
530
|
+
},
|
|
531
|
+
sitemapRoutes: ["/"]
|
|
532
|
+
});
|
|
533
|
+
function createHandler(store, options = {}) {
|
|
534
|
+
const config = options.config ?? defaultConfig;
|
|
535
|
+
return async (request) => {
|
|
536
|
+
const url = new URL(request.url);
|
|
537
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
538
|
+
const sitePaths = ["/sitemap.xml", "/llms.txt", "/"];
|
|
539
|
+
const needsScope = !sitePaths.includes(pathname) || pathname === "/";
|
|
540
|
+
let scope = { tenantId: config.tenantId, projectId: config.projectId };
|
|
541
|
+
if (needsScope && pathname !== "/") {
|
|
542
|
+
const parsed = readScope(request);
|
|
543
|
+
if (parsed instanceof Response) return parsed;
|
|
544
|
+
scope = parsed;
|
|
545
|
+
} else if (request.headers.get("x-tenant-id")) {
|
|
546
|
+
const parsed = readScope(request);
|
|
547
|
+
if (!(parsed instanceof Response)) scope = parsed;
|
|
548
|
+
}
|
|
549
|
+
const response = await dispatchRoute(request, {
|
|
550
|
+
store,
|
|
551
|
+
scope,
|
|
552
|
+
config,
|
|
553
|
+
agentModel: options.agentModel
|
|
554
|
+
});
|
|
555
|
+
if (response) return response;
|
|
556
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
export {
|
|
560
|
+
createHandler,
|
|
561
|
+
readScope
|
|
562
|
+
};
|
|
563
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/handler.ts","../src/routes.ts","../src/utils.ts"],"sourcesContent":["import \"server-only\";\n\nimport type { LanguageModel } from \"ai\";\nimport {\n defineConfig,\n type RankMySeoConfig,\n type RankStore,\n} from \"@rankmyseo/core\";\nimport { dispatchRoute } from \"./routes.js\";\nimport { readScope } from \"./utils.js\";\n\nexport type { RequestScope } from \"./utils.js\";\nexport { readScope } from \"./utils.js\";\nexport {\n buildLlmsTxt,\n buildSitemapXml,\n pageToMarkdown,\n withMarkdownNegotiation,\n} from \"./utils.js\";\n\nexport interface HandlerOptions {\n config?: RankMySeoConfig;\n agentModel?: LanguageModel;\n}\n\nconst defaultConfig = defineConfig({\n databaseUrl: \"sqlite://:memory:\",\n tenantId: \"default\",\n projectId: \"default\",\n dataSources: [{ provider: \"fixture\", default: true }],\n schedule: { cron: \"0 6 * * *\", enabled: false },\n siteFeatures: {\n sitemap: true,\n llmsTxt: true,\n collector: true,\n markdownNegotiation: true,\n blog: false,\n },\n sitemapRoutes: [\"/\"],\n});\n\nexport function createHandler(store: RankStore, options: HandlerOptions = {}) {\n const config = options.config ?? defaultConfig;\n\n return async (request: Request): Promise<Response> => {\n const url = new URL(request.url);\n const pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n\n const sitePaths = [\"/sitemap.xml\", \"/llms.txt\", \"/\"];\n const needsScope =\n !sitePaths.includes(pathname) || pathname === \"/\";\n\n let scope = { tenantId: config.tenantId, projectId: config.projectId };\n if (needsScope && pathname !== \"/\") {\n const parsed = readScope(request);\n if (parsed instanceof Response) return parsed;\n scope = parsed;\n } else if (request.headers.get(\"x-tenant-id\")) {\n const parsed = readScope(request);\n if (!(parsed instanceof Response)) scope = parsed;\n }\n\n const response = await dispatchRoute(request, {\n store,\n scope,\n config,\n agentModel: options.agentModel,\n });\n\n if (response) return response;\n return Response.json({ error: \"Not found\" }, { status: 404 });\n };\n}\n","import \"server-only\";\n\nimport { randomUUID } from \"node:crypto\";\nimport type { LanguageModel } from \"ai\";\nimport {\n buildAuditRecommendations,\n buildBlogRecommendations,\n buildReport,\n createAuditInputSchema,\n createBlogPostInputSchema,\n createKeywordInputSchema,\n createRankSnapshotInputSchema,\n dashboardConfigSchema,\n extractPageSignals,\n generateMeta,\n normalizeHttpUrl,\n pageSignalsSchema,\n projectSchema,\n runAuditChecks,\n slugify,\n snapshotRangeQuerySchema,\n updateBlogPostInputSchema,\n type RankMySeoConfig,\n type RankStore,\n type TenantScope,\n} from \"@rankmyseo/core\";\nimport { streamAgentChat } from \"@rankmyseo/agent\";\nimport {\n buildLlmsTxt,\n buildSitemapXml,\n pageToMarkdown,\n readJson,\n withMarkdownNegotiation,\n} from \"./utils.js\";\n\nexport interface RouteContext {\n store: RankStore;\n scope: TenantScope;\n config: RankMySeoConfig;\n agentModel?: LanguageModel;\n}\n\ntype RouteHandler = (\n request: Request,\n ctx: RouteContext,\n params: Record<string, string>,\n url: URL,\n) => Promise<Response>;\n\nconst routes: Array<{\n method: string;\n pattern: RegExp;\n handler: RouteHandler;\n}> = [];\n\nfunction addRoute(\n method: string,\n pattern: RegExp,\n handler: RouteHandler,\n): void {\n routes.push({ method, pattern, handler });\n}\n\naddRoute(\"GET\", /^\\/projects$/, async (_req, ctx) => {\n const data = await ctx.store.projects.list(ctx.scope);\n return Response.json({ data });\n});\n\naddRoute(\"POST\", /^\\/projects$/, async (request, ctx) => {\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const parsed = projectSchema\n .omit({ createdAt: true, updatedAt: true })\n .safeParse(body);\n if (!parsed.success) {\n return Response.json({ error: \"Invalid project\", details: parsed.error.flatten() }, { status: 400 });\n }\n const project = await ctx.store.projects.create({\n ...parsed.data,\n tenantId: ctx.scope.tenantId,\n });\n return Response.json({ data: project }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/projects\\/([^/]+)$/, async (_req, ctx, params) => {\n const project = await ctx.store.projects.getById(ctx.scope, params[1]!);\n if (!project) return Response.json({ error: \"Not found\" }, { status: 404 });\n return Response.json({ data: project });\n});\n\naddRoute(\"GET\", /^\\/keywords$/, async (_req, ctx) => {\n const data = await ctx.store.keywords.list(ctx.scope);\n return Response.json({ data });\n});\n\naddRoute(\"POST\", /^\\/keywords$/, async (request, ctx) => {\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const parsed = createKeywordInputSchema.safeParse({\n ...(body as Record<string, unknown>),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid keyword\", details: parsed.error.flatten() }, { status: 400 });\n }\n const keyword = await ctx.store.keywords.create(parsed.data);\n return Response.json({ data: keyword }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/keywords\\/([^/]+)$/, async (_req, ctx, params) => {\n const keyword = await ctx.store.keywords.getById(ctx.scope, params[1]!);\n if (!keyword) return Response.json({ error: \"Not found\" }, { status: 404 });\n return Response.json({ data: keyword });\n});\n\naddRoute(\"DELETE\", /^\\/keywords\\/([^/]+)$/, async (_req, ctx, params) => {\n const deleted = await ctx.store.keywords.delete(ctx.scope, params[1]!);\n if (!deleted) return Response.json({ error: \"Not found\" }, { status: 404 });\n return new Response(null, { status: 204 });\n});\n\naddRoute(\"POST\", /^\\/snapshots$/, async (request, ctx) => {\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const parsed = createRankSnapshotInputSchema.safeParse({\n ...(body as Record<string, unknown>),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid snapshot\", details: parsed.error.flatten() }, { status: 400 });\n }\n const snapshot = await ctx.store.snapshots.append(parsed.data);\n return Response.json({ data: snapshot }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/snapshots$/, async (_req, ctx, _params, url) => {\n const parsed = snapshotRangeQuerySchema.safeParse({\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n keywordId: url.searchParams.get(\"keywordId\") ?? undefined,\n from: url.searchParams.get(\"from\"),\n to: url.searchParams.get(\"to\"),\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid query\", details: parsed.error.flatten() }, { status: 400 });\n }\n const data = await ctx.store.snapshots.listByRange(parsed.data);\n return Response.json({ data });\n});\n\naddRoute(\"GET\", /^\\/audits$/, async (_req, ctx) => {\n const data = await ctx.store.audits.list(ctx.scope);\n return Response.json({ data });\n});\n\naddRoute(\"POST\", /^\\/audits$/, async (request, ctx) => {\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const parsed = createAuditInputSchema.safeParse({\n ...(body as Record<string, unknown>),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid audit\", details: parsed.error.flatten() }, { status: 400 });\n }\n const audit = await ctx.store.audits.create({\n ...parsed.data,\n id: randomUUID(),\n });\n return Response.json({ data: audit }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/audits\\/([^/]+)$/, async (_req, ctx, params) => {\n const audit = await ctx.store.audits.getById(ctx.scope, params[1]!);\n if (!audit) return Response.json({ error: \"Not found\" }, { status: 404 });\n return Response.json({ data: audit });\n});\n\naddRoute(\"POST\", /^\\/collect$/, async (request, ctx) => {\n if (!ctx.config.siteFeatures.collector) {\n return Response.json({ error: \"Collector disabled\" }, { status: 403 });\n }\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const parsed = pageSignalsSchema.safeParse(body);\n if (!parsed.success) {\n return Response.json({ error: \"Invalid signals\", details: parsed.error.flatten() }, { status: 400 });\n }\n const { checks, score } = runAuditChecks(parsed.data);\n const audit = await ctx.store.audits.create({\n id: randomUUID(),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n url: parsed.data.url,\n score,\n checks,\n });\n return Response.json({ data: audit }, { status: 201 });\n});\n\naddRoute(\"POST\", /^\\/scan$/, async (request, ctx) => {\n const body = await readJson<{ url?: string }>(request);\n if (body instanceof Response) return body;\n\n let target: URL;\n try {\n target = normalizeHttpUrl(String(body.url ?? \"\"));\n } catch {\n return Response.json({ error: \"A valid url is required\" }, { status: 400 });\n }\n if (target.protocol !== \"http:\" && target.protocol !== \"https:\") {\n return Response.json({ error: \"Only http(s) URLs can be scanned\" }, { status: 400 });\n }\n\n let html: string;\n try {\n const res = await fetch(target.toString(), {\n headers: { \"user-agent\": \"RankMySEO-Scanner/1.0\" },\n redirect: \"follow\",\n });\n if (!res.ok) {\n return Response.json(\n { error: `Fetch failed with status ${res.status}` },\n { status: 502 },\n );\n }\n html = await res.text();\n } catch {\n return Response.json({ error: \"Could not fetch the target URL\" }, { status: 502 });\n }\n\n const signals = extractPageSignals(html, target.toString());\n const { checks, score } = runAuditChecks(signals);\n const audit = await ctx.store.audits.create({\n id: randomUUID(),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n url: target.toString(),\n score,\n checks,\n });\n const recommendations = buildAuditRecommendations(checks);\n\n return Response.json(\n { data: { audit, signals, recommendations } },\n { status: 201 },\n );\n});\n\naddRoute(\"POST\", /^\\/meta\\/generate$/, async (request, _ctx) => {\n const body = await readJson<{\n title?: string;\n content?: string;\n targetKeyword?: string;\n url?: string;\n siteName?: string;\n }>(request);\n if (body instanceof Response) return body;\n if (!body.title || !body.title.trim()) {\n return Response.json({ error: \"title is required\" }, { status: 400 });\n }\n\n const meta = generateMeta({\n title: body.title,\n content: body.content,\n targetKeyword: body.targetKeyword,\n url: body.url,\n siteName: body.siteName,\n });\n\n const { checks, score } = runAuditChecks({\n url: body.url && /^https?:\\/\\//.test(body.url) ? body.url : \"https://example.com\",\n title: meta.metaTitle,\n metaDescription: meta.metaDescription,\n canonical: meta.canonical && /^https?:\\/\\//.test(meta.canonical) ? meta.canonical : null,\n h1Count: 1,\n hasOgTags: true,\n hasJsonLd: true,\n });\n\n return Response.json({ data: { meta, checks, score } });\n});\n\nfunction blogDisabled(ctx: RouteContext): Response | null {\n if (!ctx.config.siteFeatures.blog) {\n return Response.json({ error: \"Blog module disabled\" }, { status: 403 });\n }\n return null;\n}\n\naddRoute(\"GET\", /^\\/blog$/, async (_req, ctx) => {\n const denied = blogDisabled(ctx);\n if (denied) return denied;\n const data = await ctx.store.blog.list(ctx.scope);\n return Response.json({ data });\n});\n\naddRoute(\"POST\", /^\\/blog$/, async (request, ctx) => {\n const denied = blogDisabled(ctx);\n if (denied) return denied;\n const body = await readJson<Record<string, unknown>>(request);\n if (body instanceof Response) return body;\n const parsed = createBlogPostInputSchema.safeParse({\n ...body,\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid blog post\", details: parsed.error.flatten() }, { status: 400 });\n }\n\n const input = parsed.data;\n const slug = input.slug?.trim() || slugify(input.title);\n let { metaTitle, metaDescription } = input;\n if (!metaTitle.trim() || !metaDescription.trim()) {\n const meta = generateMeta({\n title: input.title,\n content: input.content,\n targetKeyword: input.targetKeyword,\n });\n metaTitle = metaTitle.trim() || meta.metaTitle;\n metaDescription = metaDescription.trim() || meta.metaDescription;\n }\n\n const post = await ctx.store.blog.create({\n id: randomUUID(),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n title: input.title,\n slug,\n content: input.content,\n targetKeyword: input.targetKeyword,\n intent: input.intent,\n metaTitle,\n metaDescription,\n status: input.status,\n });\n return Response.json({ data: post }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/blog\\/([^/]+)$/, async (_req, ctx, params) => {\n const denied = blogDisabled(ctx);\n if (denied) return denied;\n const post = await ctx.store.blog.getById(ctx.scope, params[1]!);\n if (!post) return Response.json({ error: \"Not found\" }, { status: 404 });\n const recommendations = buildBlogRecommendations({\n intent: post.intent,\n targetKeyword: post.targetKeyword,\n metaTitle: post.metaTitle,\n metaDescription: post.metaDescription,\n content: post.content,\n });\n return Response.json({ data: post, recommendations });\n});\n\naddRoute(\"PUT\", /^\\/blog\\/([^/]+)$/, async (request, ctx, params) => {\n const denied = blogDisabled(ctx);\n if (denied) return denied;\n const body = await readJson<Record<string, unknown>>(request);\n if (body instanceof Response) return body;\n const parsed = updateBlogPostInputSchema.safeParse(body);\n if (!parsed.success) {\n return Response.json({ error: \"Invalid blog post\", details: parsed.error.flatten() }, { status: 400 });\n }\n const updated = await ctx.store.blog.update(ctx.scope, params[1]!, parsed.data);\n if (!updated) return Response.json({ error: \"Not found\" }, { status: 404 });\n return Response.json({ data: updated });\n});\n\naddRoute(\"DELETE\", /^\\/blog\\/([^/]+)$/, async (_req, ctx, params) => {\n const denied = blogDisabled(ctx);\n if (denied) return denied;\n const deleted = await ctx.store.blog.delete(ctx.scope, params[1]!);\n if (!deleted) return Response.json({ error: \"Not found\" }, { status: 404 });\n return new Response(null, { status: 204 });\n});\n\naddRoute(\"GET\", /^\\/reports$/, async (_req, ctx) => {\n const data = await ctx.store.reports.list(ctx.scope);\n return Response.json({ data });\n});\n\naddRoute(\"POST\", /^\\/reports$/, async (request, ctx) => {\n const body = await readJson<Record<string, unknown>>(request);\n if (body instanceof Response) return body;\n const from = new Date(String(body.from));\n const to = new Date(String(body.to));\n const title = String(body.title ?? \"Report\");\n const keywords = await ctx.store.keywords.list(ctx.scope);\n const snapshots = await ctx.store.snapshots.listByRange({\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n from,\n to,\n });\n const audits = await ctx.store.audits.list(ctx.scope);\n const reportData = buildReport({\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n title,\n from,\n to,\n keywords,\n snapshots,\n audits,\n });\n const report = await ctx.store.reports.create(reportData);\n return Response.json({ data: report }, { status: 201 });\n});\n\naddRoute(\"GET\", /^\\/reports\\/([^/]+)$/, async (_req, ctx, params) => {\n const report = await ctx.store.reports.getById(ctx.scope, params[1]!);\n if (!report) return Response.json({ error: \"Not found\" }, { status: 404 });\n return Response.json({ data: report });\n});\n\naddRoute(\"GET\", /^\\/dashboard$/, async (_req, ctx) => {\n const config = await ctx.store.dashboard.get(ctx.scope);\n return Response.json({ data: config ?? null });\n});\n\naddRoute(\"PUT\", /^\\/dashboard$/, async (request, ctx) => {\n const body = await readJson<unknown>(request);\n if (body instanceof Response) return body;\n const existing = await ctx.store.dashboard.get(ctx.scope);\n const parsed = dashboardConfigSchema.safeParse({\n ...(body as Record<string, unknown>),\n id: existing?.id ?? randomUUID(),\n tenantId: ctx.scope.tenantId,\n projectId: ctx.scope.projectId,\n updatedAt: new Date(),\n });\n if (!parsed.success) {\n return Response.json({ error: \"Invalid dashboard\", details: parsed.error.flatten() }, { status: 400 });\n }\n const saved = await ctx.store.dashboard.upsert(parsed.data);\n return Response.json({ data: saved });\n});\n\naddRoute(\"POST\", /^\\/agent\\/chat$/, async (request, ctx) => {\n if (!ctx.agentModel) {\n return Response.json({ error: \"Agent model not configured\" }, { status: 503 });\n }\n const body = await readJson<{ messages?: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }> }>(request);\n if (body instanceof Response) return body;\n const result = await streamAgentChat({\n store: ctx.store,\n scope: ctx.scope,\n model: ctx.agentModel,\n messages: body.messages ?? [],\n });\n return result.toTextStreamResponse();\n});\n\naddRoute(\"GET\", /^\\/sitemap\\.xml$/, async (_req, ctx) => {\n if (!ctx.config.siteFeatures.sitemap) {\n return Response.json({ error: \"Sitemap disabled\" }, { status: 404 });\n }\n const projects = await ctx.store.projects.list(ctx.scope);\n const domain = projects[0]?.domain ?? \"example.com\";\n const baseUrl = domain.startsWith(\"http\") ? domain : `https://${domain}`;\n const xml = buildSitemapXml(ctx.config.sitemapRoutes, baseUrl);\n return new Response(xml, {\n headers: { \"Content-Type\": \"application/xml; charset=utf-8\" },\n });\n});\n\naddRoute(\"GET\", /^\\/llms\\.txt$/, async (_req, ctx) => {\n if (!ctx.config.siteFeatures.llmsTxt) {\n return Response.json({ error: \"llms.txt disabled\" }, { status: 404 });\n }\n const text = buildLlmsTxt(ctx.config);\n return new Response(text, {\n headers: { \"Content-Type\": \"text/markdown; charset=utf-8\" },\n });\n});\n\naddRoute(\"GET\", /^\\/$/, async (request, ctx, _params, url) => {\n if (!ctx.config.siteFeatures.markdownNegotiation) {\n return Response.json({ ok: true, service: \"rankmyseo\" });\n }\n const html = `<!DOCTYPE html><html><head><title>RankMySEO</title></head><body><h1>RankMySEO</h1><p>SEO toolkit API</p></body></html>`;\n const md = pageToMarkdown(url.pathname, \"RankMySEO\");\n return withMarkdownNegotiation(request, html, md, url.pathname);\n});\n\nexport async function dispatchRoute(\n request: Request,\n ctx: RouteContext,\n): Promise<Response | null> {\n const url = new URL(request.url);\n const method = request.method.toUpperCase();\n const pathname = url.pathname.replace(/\\/+$/, \"\") || \"/\";\n\n for (const route of routes) {\n if (route.method !== method) continue;\n const match = pathname.match(route.pattern);\n if (!match) continue;\n const params: Record<string, string> = {};\n match.slice(1).forEach((value, index) => {\n params[String(index + 1)] = value;\n });\n return route.handler(request, ctx, params, url);\n }\n\n return null;\n}\n","import type { RankMySeoConfig, TenantScope } from \"@rankmyseo/core\";\n\nexport interface RequestScope extends TenantScope {}\n\nexport function readScope(request: Request): RequestScope | Response {\n const tenantId = request.headers.get(\"x-tenant-id\");\n const projectId = request.headers.get(\"x-project-id\");\n\n if (!tenantId || !projectId) {\n return Response.json(\n { error: \"Missing or invalid x-tenant-id / x-project-id headers\" },\n { status: 400 },\n );\n }\n\n return { tenantId, projectId };\n}\n\nexport async function readJson<T>(request: Request): Promise<T | Response> {\n try {\n return (await request.json()) as T;\n } catch {\n return Response.json({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n}\n\nexport function acceptsMarkdown(request: Request): boolean {\n const accept = request.headers.get(\"accept\") ?? \"\";\n return accept.includes(\"text/markdown\");\n}\n\nexport interface SiteFeatureContext {\n config: RankMySeoConfig;\n scope: TenantScope;\n pathname: string;\n}\n\nexport function buildSitemapXml(routes: string[], baseUrl: string): string {\n const urls = routes\n .map(\n (route) =>\n ` <url><loc>${baseUrl}${route === \"/\" ? \"\" : route}</loc></url>`,\n )\n .join(\"\\n\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n${urls}\\n</urlset>`;\n}\n\nexport function buildLlmsTxt(config: RankMySeoConfig): string {\n const name = config.llmsTxt?.projectName ?? \"RankMySEO Project\";\n const summary =\n config.llmsTxt?.summary ??\n \"SEO tracking, audits, and rank history for this site.\";\n const links = config.llmsTxt?.links ?? [\n { title: \"Documentation\", url: \"/docs.md\" },\n ];\n\n const linkBlock = links.map((l) => `- [${l.title}](${l.url})`).join(\"\\n\");\n return `# ${name}\\n\\n> ${summary}\\n\\n## Resources\\n\\n${linkBlock}\\n`;\n}\n\nexport function pageToMarkdown(pathname: string, title: string): string {\n return `# ${title}\\n\\nPath: \\`${pathname}\\`\\n\\nThis page is available as Markdown for AI agents.\\n`;\n}\n\nexport function withMarkdownNegotiation(\n request: Request,\n html: string,\n markdown: string,\n pathname: string,\n): Response {\n const wantsMarkdown = acceptsMarkdown(request);\n const accept = request.headers.get(\"accept\") ?? \"\";\n\n if (wantsMarkdown) {\n return new Response(markdown, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/markdown; charset=utf-8\",\n Vary: \"Accept\",\n Link: `<${pathname}?format=html>; rel=\"alternate\"; type=\"text/html\"`,\n },\n });\n }\n\n if (accept && !accept.includes(\"*/*\") && !accept.includes(\"text/html\")) {\n return Response.json(\n { error: \"Not acceptable\", supported: [\"text/html\", \"text/markdown\"] },\n { status: 406, headers: { Vary: \"Accept\" } },\n );\n }\n\n return new Response(html, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n Vary: \"Accept\",\n Link: `<${pathname}>; rel=\"alternate\"; type=\"text/markdown\"`,\n },\n });\n}\n"],"mappings":"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","names":["routes"]}
|