@seoengine.ai/next-llm-ready 1.0.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.
@@ -0,0 +1,613 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ // src/api/llms-txt-handler.ts
4
+
5
+ // src/server/generate-llms-txt.ts
6
+ function generateLLMsTxt(config) {
7
+ const parts = [];
8
+ parts.push(`# ${config.siteName}`);
9
+ parts.push("");
10
+ if (config.siteDescription) {
11
+ parts.push(`> ${config.siteDescription}`);
12
+ parts.push("");
13
+ }
14
+ if (config.headerText) {
15
+ parts.push(config.headerText);
16
+ } else {
17
+ parts.push("This file provides LLM-friendly access to the content on this website.");
18
+ }
19
+ parts.push("");
20
+ parts.push("---");
21
+ parts.push(`- **Site URL**: ${config.siteUrl}`);
22
+ parts.push("- **Format**: Append `?llm=1` to any page URL to get markdown");
23
+ parts.push("---");
24
+ parts.push("");
25
+ if (config.content.length > 0) {
26
+ parts.push("## Available Content");
27
+ parts.push("");
28
+ for (const item of config.content) {
29
+ const llmUrl = appendQueryParam(item.url, "llm", "1");
30
+ parts.push(`### [${item.title}](${llmUrl})`);
31
+ if (item.type) {
32
+ parts.push(`- **Type**: ${item.type}`);
33
+ }
34
+ if (item.date) {
35
+ parts.push(`- **Date**: ${item.date}`);
36
+ }
37
+ if (item.description) {
38
+ parts.push(`- **Description**: ${item.description}`);
39
+ }
40
+ parts.push(`- **Markdown URL**: ${llmUrl}`);
41
+ parts.push("");
42
+ }
43
+ }
44
+ parts.push("---");
45
+ parts.push("");
46
+ if (config.footerText) {
47
+ parts.push(config.footerText);
48
+ } else {
49
+ parts.push("*Generated by [next-llm-ready](https://seoengine.ai) - Make your content AI-ready*");
50
+ }
51
+ return parts.join("\n");
52
+ }
53
+ function appendQueryParam(url, key, value) {
54
+ const separator = url.includes("?") ? "&" : "?";
55
+ return `${url}${separator}${key}=${value}`;
56
+ }
57
+
58
+ // src/api/llms-txt-handler.ts
59
+ function createLLMsTxtHandler(options) {
60
+ return async function handler(request) {
61
+ try {
62
+ const siteConfig = await options.getSiteConfig();
63
+ const content = await options.getContent();
64
+ const config = {
65
+ siteName: siteConfig.siteName,
66
+ siteDescription: siteConfig.siteDescription,
67
+ siteUrl: siteConfig.siteUrl,
68
+ content,
69
+ headerText: options.headerText,
70
+ footerText: options.footerText
71
+ };
72
+ const llmsTxt = generateLLMsTxt(config);
73
+ return new NextResponse(llmsTxt, {
74
+ status: 200,
75
+ headers: {
76
+ "Content-Type": "text/plain; charset=utf-8",
77
+ "Cache-Control": options.cacheControl || "public, max-age=3600",
78
+ "X-Robots-Tag": "noindex"
79
+ }
80
+ });
81
+ } catch (error) {
82
+ console.error("Error generating llms.txt:", error);
83
+ return new NextResponse("Error generating llms.txt", { status: 500 });
84
+ }
85
+ };
86
+ }
87
+ function createLLMsTxtPageHandler(options) {
88
+ return async function handler(req, res) {
89
+ if (req.method !== "GET") {
90
+ res.status(405).end("Method Not Allowed");
91
+ return;
92
+ }
93
+ try {
94
+ const siteConfig = await options.getSiteConfig();
95
+ const content = await options.getContent();
96
+ const config = {
97
+ siteName: siteConfig.siteName,
98
+ siteDescription: siteConfig.siteDescription,
99
+ siteUrl: siteConfig.siteUrl,
100
+ content,
101
+ headerText: options.headerText,
102
+ footerText: options.footerText
103
+ };
104
+ const llmsTxt = generateLLMsTxt(config);
105
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
106
+ res.setHeader("Cache-Control", options.cacheControl || "public, max-age=3600");
107
+ res.setHeader("X-Robots-Tag", "noindex");
108
+ res.status(200).end(llmsTxt);
109
+ } catch (error) {
110
+ console.error("Error generating llms.txt:", error);
111
+ res.status(500).end("Error generating llms.txt");
112
+ }
113
+ };
114
+ }
115
+
116
+ // src/utils/html-to-markdown.ts
117
+ var defaultOptions = {
118
+ preserveLineBreaks: true,
119
+ convertImages: true,
120
+ convertLinks: true,
121
+ stripScripts: true,
122
+ customHandlers: {}
123
+ };
124
+ function htmlToMarkdown(html, options = {}) {
125
+ const opts = { ...defaultOptions, ...options };
126
+ if (typeof window === "undefined") {
127
+ return serverSideConvert(html, opts);
128
+ }
129
+ const doc = new DOMParser().parseFromString(html, "text/html");
130
+ return convertNode(doc.body, opts);
131
+ }
132
+ function serverSideConvert(html, opts) {
133
+ let markdown = html;
134
+ if (opts.stripScripts) {
135
+ markdown = markdown.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
136
+ markdown = markdown.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "");
137
+ }
138
+ markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, "\n# $1\n");
139
+ markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, "\n## $1\n");
140
+ markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, "\n### $1\n");
141
+ markdown = markdown.replace(/<h4[^>]*>(.*?)<\/h4>/gi, "\n#### $1\n");
142
+ markdown = markdown.replace(/<h5[^>]*>(.*?)<\/h5>/gi, "\n##### $1\n");
143
+ markdown = markdown.replace(/<h6[^>]*>(.*?)<\/h6>/gi, "\n###### $1\n");
144
+ markdown = markdown.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
145
+ markdown = markdown.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
146
+ if (opts.convertLinks) {
147
+ markdown = markdown.replace(/<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
148
+ }
149
+ if (opts.convertImages) {
150
+ markdown = markdown.replace(
151
+ /<img[^>]*src=["']([^"']*)["'][^>]*alt=["']([^"']*)["'][^>]*\/?>/gi,
152
+ "![$2]($1)"
153
+ );
154
+ markdown = markdown.replace(
155
+ /<img[^>]*alt=["']([^"']*)["'][^>]*src=["']([^"']*)["'][^>]*\/?>/gi,
156
+ "![$1]($2)"
157
+ );
158
+ markdown = markdown.replace(/<img[^>]*src=["']([^"']*)["'][^>]*\/?>/gi, "![]($1)");
159
+ }
160
+ markdown = markdown.replace(/<ul[^>]*>/gi, "\n");
161
+ markdown = markdown.replace(/<\/ul>/gi, "\n");
162
+ markdown = markdown.replace(/<ol[^>]*>/gi, "\n");
163
+ markdown = markdown.replace(/<\/ol>/gi, "\n");
164
+ markdown = markdown.replace(/<li[^>]*>(.*?)<\/li>/gi, "- $1\n");
165
+ markdown = markdown.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, (_, content) => {
166
+ return "\n> " + content.trim().replace(/\n/g, "\n> ") + "\n";
167
+ });
168
+ markdown = markdown.replace(/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/gis, "\n```\n$1\n```\n");
169
+ markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
170
+ markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, "\n$1\n");
171
+ markdown = markdown.replace(/<br\s*\/?>/gi, "\n");
172
+ markdown = markdown.replace(/<hr[^>]*\/?>/gi, "\n---\n");
173
+ markdown = markdown.replace(/<[^>]+>/g, "");
174
+ markdown = decodeHTMLEntities(markdown);
175
+ markdown = markdown.replace(/\n{3,}/g, "\n\n");
176
+ markdown = markdown.trim();
177
+ return markdown;
178
+ }
179
+ function convertNode(node, opts) {
180
+ if (node.nodeType === Node.TEXT_NODE) {
181
+ return node.textContent || "";
182
+ }
183
+ if (node.nodeType !== Node.ELEMENT_NODE) {
184
+ return "";
185
+ }
186
+ const element = node;
187
+ const tagName = element.tagName.toLowerCase();
188
+ if (opts.customHandlers?.[tagName]) {
189
+ return opts.customHandlers[tagName](element);
190
+ }
191
+ if (opts.stripScripts && (tagName === "script" || tagName === "style")) {
192
+ return "";
193
+ }
194
+ const childContent = Array.from(element.childNodes).map((child) => convertNode(child, opts)).join("");
195
+ switch (tagName) {
196
+ case "h1":
197
+ return `
198
+ # ${childContent.trim()}
199
+ `;
200
+ case "h2":
201
+ return `
202
+ ## ${childContent.trim()}
203
+ `;
204
+ case "h3":
205
+ return `
206
+ ### ${childContent.trim()}
207
+ `;
208
+ case "h4":
209
+ return `
210
+ #### ${childContent.trim()}
211
+ `;
212
+ case "h5":
213
+ return `
214
+ ##### ${childContent.trim()}
215
+ `;
216
+ case "h6":
217
+ return `
218
+ ###### ${childContent.trim()}
219
+ `;
220
+ case "p":
221
+ return `
222
+ ${childContent.trim()}
223
+ `;
224
+ case "br":
225
+ return opts.preserveLineBreaks ? "\n" : " ";
226
+ case "strong":
227
+ case "b":
228
+ return `**${childContent}**`;
229
+ case "em":
230
+ case "i":
231
+ return `*${childContent}*`;
232
+ case "u":
233
+ return `_${childContent}_`;
234
+ case "s":
235
+ case "strike":
236
+ case "del":
237
+ return `~~${childContent}~~`;
238
+ case "a":
239
+ if (opts.convertLinks) {
240
+ const href = element.getAttribute("href") || "";
241
+ return `[${childContent}](${href})`;
242
+ }
243
+ return childContent;
244
+ case "img":
245
+ if (opts.convertImages) {
246
+ const src = element.getAttribute("src") || "";
247
+ const alt = element.getAttribute("alt") || "";
248
+ return `![${alt}](${src})`;
249
+ }
250
+ return "";
251
+ case "ul":
252
+ return `
253
+ ${childContent}
254
+ `;
255
+ case "ol":
256
+ return `
257
+ ${childContent}
258
+ `;
259
+ case "li":
260
+ return `- ${childContent.trim()}
261
+ `;
262
+ case "blockquote":
263
+ return `
264
+ > ${childContent.trim().replace(/\n/g, "\n> ")}
265
+ `;
266
+ case "pre":
267
+ const codeElement = element.querySelector("code");
268
+ const lang = codeElement?.className.match(/language-(\w+)/)?.[1] || "";
269
+ const code = codeElement?.textContent || childContent;
270
+ return `
271
+ \`\`\`${lang}
272
+ ${code.trim()}
273
+ \`\`\`
274
+ `;
275
+ case "code":
276
+ if (element.parentElement?.tagName.toLowerCase() === "pre") {
277
+ return childContent;
278
+ }
279
+ return `\`${childContent}\``;
280
+ case "hr":
281
+ return "\n---\n";
282
+ case "table":
283
+ return convertTable(element);
284
+ case "div":
285
+ case "section":
286
+ case "article":
287
+ case "main":
288
+ case "aside":
289
+ case "header":
290
+ case "footer":
291
+ case "nav":
292
+ return childContent;
293
+ default:
294
+ return childContent;
295
+ }
296
+ }
297
+ function convertTable(table) {
298
+ const rows = table.querySelectorAll("tr");
299
+ if (rows.length === 0) return "";
300
+ let markdown = "\n";
301
+ let headerProcessed = false;
302
+ rows.forEach((row, index) => {
303
+ const cells = row.querySelectorAll("th, td");
304
+ const rowContent = Array.from(cells).map((cell) => cell.textContent?.trim() || "").join(" | ");
305
+ markdown += `| ${rowContent} |
306
+ `;
307
+ if (!headerProcessed && (row.querySelector("th") || index === 0)) {
308
+ const separator = Array.from(cells).map(() => "---").join(" | ");
309
+ markdown += `| ${separator} |
310
+ `;
311
+ headerProcessed = true;
312
+ }
313
+ });
314
+ return markdown + "\n";
315
+ }
316
+ function decodeHTMLEntities(text) {
317
+ const entities = {
318
+ "&amp;": "&",
319
+ "&lt;": "<",
320
+ "&gt;": ">",
321
+ "&quot;": '"',
322
+ "&#39;": "'",
323
+ "&apos;": "'",
324
+ "&nbsp;": " ",
325
+ "&mdash;": "\u2014",
326
+ "&ndash;": "\u2013",
327
+ "&hellip;": "\u2026",
328
+ "&copy;": "\xA9",
329
+ "&reg;": "\xAE",
330
+ "&trade;": "\u2122"
331
+ };
332
+ let result = text;
333
+ for (const [entity, char] of Object.entries(entities)) {
334
+ result = result.replace(new RegExp(entity, "g"), char);
335
+ }
336
+ result = result.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)));
337
+ result = result.replace(
338
+ /&#x([0-9a-f]+);/gi,
339
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
340
+ );
341
+ return result;
342
+ }
343
+ function countWords(text) {
344
+ return text.replace(/[^\w\s]/g, "").split(/\s+/).filter((word) => word.length > 0).length;
345
+ }
346
+ function calculateReadingTime(text, wordsPerMinute = 200) {
347
+ const words = countWords(text);
348
+ return Math.ceil(words / wordsPerMinute);
349
+ }
350
+
351
+ // src/server/generate-markdown.ts
352
+ function generateMarkdown(content) {
353
+ const parts = [];
354
+ if (content.promptPrefix?.trim()) {
355
+ parts.push(content.promptPrefix.trim());
356
+ parts.push("");
357
+ }
358
+ parts.push(`# ${content.title}`);
359
+ parts.push("");
360
+ if (content.excerpt) {
361
+ parts.push(`> ${content.excerpt}`);
362
+ parts.push("");
363
+ }
364
+ parts.push("---");
365
+ parts.push(`- **Source**: ${content.url}`);
366
+ if (content.date) {
367
+ parts.push(`- **Date**: ${content.date}`);
368
+ }
369
+ if (content.modifiedDate) {
370
+ parts.push(`- **Modified**: ${content.modifiedDate}`);
371
+ }
372
+ if (content.author) {
373
+ parts.push(`- **Author**: ${content.author}`);
374
+ }
375
+ if (content.categories?.length) {
376
+ parts.push(`- **Categories**: ${content.categories.join(", ")}`);
377
+ }
378
+ if (content.tags?.length) {
379
+ parts.push(`- **Tags**: ${content.tags.join(", ")}`);
380
+ }
381
+ if (content.readingTime) {
382
+ parts.push(`- **Reading Time**: ${content.readingTime} min`);
383
+ }
384
+ parts.push("---");
385
+ parts.push("");
386
+ if (content.content) {
387
+ const contentMarkdown = isHTML(content.content) ? htmlToMarkdown(content.content) : content.content;
388
+ parts.push(contentMarkdown);
389
+ }
390
+ const markdown = parts.join("\n").trim();
391
+ const wordCount = countWords(markdown);
392
+ const readingTime = calculateReadingTime(markdown);
393
+ const headings = extractMarkdownHeadings(markdown);
394
+ return {
395
+ markdown,
396
+ wordCount,
397
+ readingTime,
398
+ headings
399
+ };
400
+ }
401
+ function generateMarkdownString(content) {
402
+ return generateMarkdown(content).markdown;
403
+ }
404
+ function isHTML(str) {
405
+ return /<[a-z][\s\S]*>/i.test(str);
406
+ }
407
+ function extractMarkdownHeadings(markdown) {
408
+ const headings = [];
409
+ const lines = markdown.split("\n");
410
+ let index = 0;
411
+ for (const line of lines) {
412
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
413
+ if (match) {
414
+ const level = match[1].length;
415
+ const text = match[2].trim();
416
+ const id = generateSlug(text, index);
417
+ headings.push({ id, text, level });
418
+ index++;
419
+ }
420
+ }
421
+ return headings;
422
+ }
423
+ function generateSlug(text, index) {
424
+ const slug = text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
425
+ return slug || `heading-${index}`;
426
+ }
427
+
428
+ // src/api/markdown-handler.ts
429
+ function withLLMParam(options) {
430
+ return async function middleware(request) {
431
+ const url = new URL(request.url);
432
+ const llmParam = url.searchParams.get("llm");
433
+ if (llmParam !== "1") {
434
+ return void 0;
435
+ }
436
+ try {
437
+ const pathname = url.pathname;
438
+ const content = await options.getContent(pathname);
439
+ if (!content) {
440
+ return new NextResponse("Content not found", { status: 404 });
441
+ }
442
+ const markdown = generateMarkdownString(content);
443
+ return new NextResponse(markdown, {
444
+ status: 200,
445
+ headers: {
446
+ "Content-Type": "text/markdown; charset=utf-8",
447
+ "Cache-Control": options.cacheControl || "public, max-age=3600",
448
+ "X-Robots-Tag": "noindex",
449
+ ...options.cors && {
450
+ "Access-Control-Allow-Origin": "*",
451
+ "Access-Control-Allow-Methods": "GET"
452
+ }
453
+ }
454
+ });
455
+ } catch (error) {
456
+ console.error("Error generating markdown:", error);
457
+ return new NextResponse("Error generating markdown", { status: 500 });
458
+ }
459
+ };
460
+ }
461
+ function createMarkdownHandler(options) {
462
+ return async function handler(request, { params }) {
463
+ try {
464
+ const slug = params?.slug || "";
465
+ const content = await options.getContent(slug);
466
+ if (!content) {
467
+ return new NextResponse("Content not found", {
468
+ status: 404,
469
+ headers: { "Content-Type": "text/plain" }
470
+ });
471
+ }
472
+ const markdown = generateMarkdownString(content);
473
+ return new NextResponse(markdown, {
474
+ status: 200,
475
+ headers: {
476
+ "Content-Type": "text/markdown; charset=utf-8",
477
+ "Cache-Control": options.cacheControl || "public, max-age=3600",
478
+ "X-Robots-Tag": "noindex",
479
+ ...options.cors && {
480
+ "Access-Control-Allow-Origin": "*",
481
+ "Access-Control-Allow-Methods": "GET"
482
+ }
483
+ }
484
+ });
485
+ } catch (error) {
486
+ console.error("Error generating markdown:", error);
487
+ return new NextResponse("Error generating markdown", { status: 500 });
488
+ }
489
+ };
490
+ }
491
+ function hasLLMParam(request) {
492
+ const url = new URL(request.url);
493
+ return url.searchParams.get("llm") === "1";
494
+ }
495
+ function createInMemoryStorage() {
496
+ const events = [];
497
+ return {
498
+ save: async (event) => {
499
+ events.push(event);
500
+ },
501
+ getAll: async () => [...events],
502
+ clear: async () => {
503
+ events.length = 0;
504
+ }
505
+ };
506
+ }
507
+ function createAnalyticsHandler(options) {
508
+ const requestCounts = /* @__PURE__ */ new Map();
509
+ return async function handler(request) {
510
+ if (request.method === "OPTIONS") {
511
+ return new NextResponse(null, {
512
+ status: 204,
513
+ headers: {
514
+ "Access-Control-Allow-Origin": "*",
515
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
516
+ "Access-Control-Allow-Headers": "Content-Type"
517
+ }
518
+ });
519
+ }
520
+ if (request.method !== "POST") {
521
+ return new NextResponse("Method Not Allowed", { status: 405 });
522
+ }
523
+ if (options.rateLimit) {
524
+ const clientIp = request.headers.get("x-forwarded-for") || "unknown";
525
+ const now = Date.now();
526
+ const windowMs = 6e4;
527
+ const clientData = requestCounts.get(clientIp) || { count: 0, resetAt: now + windowMs };
528
+ if (now > clientData.resetAt) {
529
+ clientData.count = 0;
530
+ clientData.resetAt = now + windowMs;
531
+ }
532
+ clientData.count++;
533
+ requestCounts.set(clientIp, clientData);
534
+ if (clientData.count > options.rateLimit) {
535
+ return new NextResponse("Rate limit exceeded", {
536
+ status: 429,
537
+ headers: {
538
+ "Retry-After": Math.ceil((clientData.resetAt - now) / 1e3).toString()
539
+ }
540
+ });
541
+ }
542
+ }
543
+ try {
544
+ const body = await request.json();
545
+ const event = {
546
+ action: body.action || "copy",
547
+ contentId: body.contentId,
548
+ url: body.url || request.headers.get("referer"),
549
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
550
+ metadata: body.metadata
551
+ };
552
+ await options.storage.save(event);
553
+ const headers = {
554
+ "Content-Type": "application/json"
555
+ };
556
+ if (options.cors) {
557
+ headers["Access-Control-Allow-Origin"] = "*";
558
+ }
559
+ return NextResponse.json({ success: true, event }, { headers });
560
+ } catch (error) {
561
+ console.error("Analytics tracking error:", error);
562
+ return NextResponse.json({ success: false, error: "Failed to track event" }, { status: 500 });
563
+ }
564
+ };
565
+ }
566
+ function createAnalyticsPageHandler(options) {
567
+ return async function handler(req, res) {
568
+ if (req.method !== "POST") {
569
+ res.status(405).end("Method Not Allowed");
570
+ return;
571
+ }
572
+ try {
573
+ const body = req.body;
574
+ const event = {
575
+ action: body?.action || "copy",
576
+ contentId: body?.contentId,
577
+ url: body?.url || req.headers?.referer,
578
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
579
+ metadata: body?.metadata
580
+ };
581
+ await options.storage.save(event);
582
+ if (options.cors) {
583
+ res.setHeader("Access-Control-Allow-Origin", "*");
584
+ }
585
+ res.status(200).json({ success: true, event });
586
+ } catch (error) {
587
+ console.error("Analytics tracking error:", error);
588
+ res.status(500).json({ success: false, error: "Failed to track event" });
589
+ }
590
+ };
591
+ }
592
+ async function aggregateEvents(storage) {
593
+ const events = await storage.getAll();
594
+ const counts = {
595
+ copy: 0,
596
+ view: 0,
597
+ download: 0
598
+ };
599
+ for (const event of events) {
600
+ if (event.action in counts) {
601
+ counts[event.action]++;
602
+ }
603
+ }
604
+ return counts;
605
+ }
606
+ async function getEventsForContent(storage, contentId) {
607
+ const events = await storage.getAll();
608
+ return events.filter((event) => event.contentId === contentId);
609
+ }
610
+
611
+ export { aggregateEvents, createAnalyticsHandler, createAnalyticsPageHandler, createInMemoryStorage, createLLMsTxtHandler, createLLMsTxtPageHandler, createMarkdownHandler, getEventsForContent, hasLLMParam, withLLMParam };
612
+ //# sourceMappingURL=index.js.map
613
+ //# sourceMappingURL=index.js.map