@edtools/cli 0.6.1 → 0.7.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.
Files changed (39) hide show
  1. package/dist/adapters/html/index.d.ts +2 -2
  2. package/dist/adapters/html/index.d.ts.map +1 -1
  3. package/dist/adapters/html/index.js +19 -2
  4. package/dist/adapters/html/index.js.map +1 -1
  5. package/dist/adapters/html/templates/base-layout.ejs +173 -0
  6. package/dist/adapters/html/templates/blog-index.html.ejs +170 -101
  7. package/dist/adapters/html/templates/blog-post-enhanced.ejs +655 -0
  8. package/dist/adapters/html/templates/blog-post-new.ejs +612 -0
  9. package/dist/adapters/html/templates/components/footer.ejs +330 -0
  10. package/dist/adapters/html/templates/components/header.ejs +208 -0
  11. package/dist/adapters/html/templates/components/social-share.ejs +202 -0
  12. package/dist/chunk-5N3D47CJ.js +823 -0
  13. package/dist/chunk-TROAGFSZ.js +824 -0
  14. package/dist/chunk-U77FH5BI.js +823 -0
  15. package/dist/cli/commands/config.d.ts +17 -0
  16. package/dist/cli/commands/config.d.ts.map +1 -0
  17. package/dist/cli/commands/config.js +140 -0
  18. package/dist/cli/commands/config.js.map +1 -0
  19. package/dist/cli/commands/generate.d.ts +1 -1
  20. package/dist/cli/commands/generate.d.ts.map +1 -1
  21. package/dist/cli/commands/generate.js +74 -17
  22. package/dist/cli/commands/generate.js.map +1 -1
  23. package/dist/cli/commands/init.d.ts.map +1 -1
  24. package/dist/cli/commands/init.js +114 -0
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/index.js +567 -265
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/generator.d.ts +4 -1
  29. package/dist/core/generator.d.ts.map +1 -1
  30. package/dist/core/generator.js +114 -3
  31. package/dist/core/generator.js.map +1 -1
  32. package/dist/index.d.ts +87 -3
  33. package/dist/index.js +1 -1
  34. package/dist/types/adapter.d.ts +17 -2
  35. package/dist/types/adapter.d.ts.map +1 -1
  36. package/dist/types/adapter.js.map +1 -1
  37. package/dist/types/content.d.ts +66 -0
  38. package/dist/types/content.d.ts.map +1 -1
  39. package/package.json +2 -2
@@ -0,0 +1,824 @@
1
+ import {
2
+ calculateReadTime,
3
+ calculateWordCount,
4
+ extractShortDescription,
5
+ generateTimestamp,
6
+ generateUUID
7
+ } from "./chunk-INVECVSW.js";
8
+
9
+ // src/types/adapter.ts
10
+ var AdapterRegistry = class {
11
+ adapters;
12
+ constructor() {
13
+ this.adapters = /* @__PURE__ */ new Map();
14
+ }
15
+ register(adapter) {
16
+ this.adapters.set(adapter.name, adapter);
17
+ }
18
+ get(name) {
19
+ return this.adapters.get(name);
20
+ }
21
+ async detectAdapter(projectPath) {
22
+ for (const adapter of this.adapters.values()) {
23
+ if (await adapter.detect(projectPath)) {
24
+ return adapter;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ list() {
30
+ return Array.from(this.adapters.keys());
31
+ }
32
+ };
33
+
34
+ // src/adapters/html/index.ts
35
+ import fs from "fs-extra";
36
+ import path from "path";
37
+ import { fileURLToPath } from "url";
38
+ import ejs from "ejs";
39
+ import { marked } from "marked";
40
+ var __filename = fileURLToPath(import.meta.url);
41
+ var __dirname = path.dirname(__filename);
42
+ var HTMLAdapter = class {
43
+ name = "html";
44
+ async detect(projectPath) {
45
+ try {
46
+ const files = await fs.readdir(projectPath);
47
+ const hasHtmlInRoot = files.some((f) => f.endsWith(".html"));
48
+ const hasIndexHtml = await fs.pathExists(path.join(projectPath, "index.html"));
49
+ const commonDirs = ["public", "dist", "build", "src"];
50
+ for (const dir of commonDirs) {
51
+ const dirPath = path.join(projectPath, dir);
52
+ if (await fs.pathExists(dirPath)) {
53
+ const dirFiles = await fs.readdir(dirPath);
54
+ const hasHtmlInDir = dirFiles.some((f) => f.endsWith(".html"));
55
+ if (hasHtmlInDir) {
56
+ return true;
57
+ }
58
+ }
59
+ }
60
+ return hasHtmlInRoot || hasIndexHtml;
61
+ } catch (error) {
62
+ return false;
63
+ }
64
+ }
65
+ async render(content, data) {
66
+ const templatePath = path.join(__dirname, "templates", "blog-post-enhanced.ejs");
67
+ if (!await fs.pathExists(templatePath)) {
68
+ throw new Error(`Template not found at: ${templatePath}`);
69
+ }
70
+ const template = await fs.readFile(templatePath, "utf-8");
71
+ const blogConfig = data?.blogConfig || {
72
+ name: content.metadata.author || "Blog",
73
+ description: "",
74
+ language: "es",
75
+ websiteUrl: "",
76
+ navigation: [],
77
+ primaryColor: "#2563eb",
78
+ textColor: "#1f2937",
79
+ textLight: "#6b7280",
80
+ borderColor: "#e5e7eb",
81
+ bgLight: "#f9fafb",
82
+ allowAICrawlers: true
83
+ };
84
+ const templateData = {
85
+ metadata: content.metadata,
86
+ schemaOrg: JSON.stringify(content.schemaOrg, null, 2),
87
+ intro: await this.markdownToHTML(content.content.intro),
88
+ sections: await Promise.all(
89
+ content.content.sections.map(async (section) => ({
90
+ ...section,
91
+ content: await this.renderSectionContent(section)
92
+ }))
93
+ ),
94
+ conclusion: await this.markdownToHTML(content.content.conclusion),
95
+ cta: content.content.cta,
96
+ relatedPosts: content.relatedPosts || [],
97
+ seoScore: content.seo.score,
98
+ // New data
99
+ blogConfig,
100
+ tableOfContents: data?.tableOfContents || [],
101
+ prevPost: data?.prevPost,
102
+ nextPost: data?.nextPost
103
+ };
104
+ const html = ejs.render(template, templateData);
105
+ return html;
106
+ }
107
+ async write(output, outputPath) {
108
+ await fs.ensureDir(path.dirname(outputPath));
109
+ await fs.writeFile(outputPath, output, "utf-8");
110
+ }
111
+ getFileExtension() {
112
+ return ".html";
113
+ }
114
+ transformPath(basePath) {
115
+ if (!basePath.endsWith(".html")) {
116
+ return basePath + ".html";
117
+ }
118
+ return basePath;
119
+ }
120
+ /**
121
+ * Convert markdown to HTML
122
+ */
123
+ async markdownToHTML(markdown) {
124
+ return marked(markdown);
125
+ }
126
+ /**
127
+ * Render section content based on type
128
+ */
129
+ async renderSectionContent(section) {
130
+ switch (section.type) {
131
+ case "text":
132
+ return await this.markdownToHTML(section.content);
133
+ case "comparison":
134
+ return this.renderComparisonTable(section.data);
135
+ case "list":
136
+ return this.renderList(section.data);
137
+ case "code":
138
+ return this.renderCode(section.data);
139
+ default:
140
+ return await this.markdownToHTML(section.content);
141
+ }
142
+ }
143
+ /**
144
+ * Render comparison table
145
+ */
146
+ renderComparisonTable(data) {
147
+ if (!data || !data.headers || !data.rows) {
148
+ return "";
149
+ }
150
+ let html = '<table class="comparison-table">\n';
151
+ html += " <thead>\n <tr>\n";
152
+ data.headers.forEach((header) => {
153
+ html += ` <th>${header}</th>
154
+ `;
155
+ });
156
+ html += " </tr>\n </thead>\n";
157
+ html += " <tbody>\n";
158
+ data.rows.forEach((row) => {
159
+ html += " <tr>\n";
160
+ row.forEach((cell) => {
161
+ html += ` <td>${cell}</td>
162
+ `;
163
+ });
164
+ html += " </tr>\n";
165
+ });
166
+ html += " </tbody>\n";
167
+ html += "</table>";
168
+ return html;
169
+ }
170
+ /**
171
+ * Render list
172
+ */
173
+ renderList(data) {
174
+ if (!data || !data.items) {
175
+ return "";
176
+ }
177
+ const tag = data.ordered ? "ol" : "ul";
178
+ let html = `<${tag} class="content-list">
179
+ `;
180
+ data.items.forEach((item) => {
181
+ html += " <li>\n";
182
+ html += ` <strong>${item.title}</strong>
183
+ `;
184
+ if (item.description) {
185
+ html += ` <p>${item.description}</p>
186
+ `;
187
+ }
188
+ html += " </li>\n";
189
+ });
190
+ html += `</${tag}>`;
191
+ return html;
192
+ }
193
+ /**
194
+ * Render code block
195
+ */
196
+ renderCode(data) {
197
+ if (!data || !data.code) {
198
+ return "";
199
+ }
200
+ let html = '<div class="code-block">\n';
201
+ if (data.caption) {
202
+ html += ` <div class="code-caption">${data.caption}</div>
203
+ `;
204
+ }
205
+ html += ` <pre><code class="language-${data.language || "plaintext"}">${this.escapeHtml(data.code)}</code></pre>
206
+ `;
207
+ html += "</div>";
208
+ return html;
209
+ }
210
+ /**
211
+ * Escape HTML entities
212
+ */
213
+ escapeHtml(text) {
214
+ const map = {
215
+ "&": "&amp;",
216
+ "<": "&lt;",
217
+ ">": "&gt;",
218
+ '"': "&quot;",
219
+ "'": "&#039;"
220
+ };
221
+ return text.replace(/[&<>"']/g, (m) => map[m]);
222
+ }
223
+ };
224
+
225
+ // src/core/generator.ts
226
+ import Anthropic from "@anthropic-ai/sdk";
227
+ import OpenAI from "openai";
228
+ import path2 from "path";
229
+ import { fileURLToPath as fileURLToPath2 } from "url";
230
+ import fs2 from "fs-extra";
231
+ import slugify from "slugify";
232
+ var ContentGenerator = class {
233
+ anthropic = null;
234
+ openai = null;
235
+ provider;
236
+ adapters;
237
+ constructor(apiKey, provider = "anthropic") {
238
+ this.provider = provider;
239
+ if (provider === "anthropic") {
240
+ this.anthropic = new Anthropic({
241
+ apiKey: apiKey || process.env.ANTHROPIC_API_KEY || ""
242
+ });
243
+ } else if (provider === "openai") {
244
+ this.openai = new OpenAI({
245
+ apiKey: apiKey || process.env.OPENAI_API_KEY || ""
246
+ });
247
+ }
248
+ this.adapters = new AdapterRegistry();
249
+ this.adapters.register(new HTMLAdapter());
250
+ }
251
+ /**
252
+ * Main generate method
253
+ */
254
+ async generate(config) {
255
+ const results = {
256
+ success: true,
257
+ posts: [],
258
+ warnings: [],
259
+ errors: [],
260
+ dryRun: config.dryRun
261
+ };
262
+ const allPostsContent = [];
263
+ const { calculateAvgReadTime } = await import("./content-helpers-TXVEJMQK.js");
264
+ try {
265
+ const adapter = await this.adapters.detectAdapter(config.projectPath);
266
+ if (!adapter) {
267
+ throw new Error("No suitable adapter found for this project");
268
+ }
269
+ console.log(`\u2713 Using adapter: ${adapter.name}`);
270
+ const topics = config.topics || await this.generateTopics(config.productInfo, config.count || 3);
271
+ const blogConfig = config.blogConfig || {
272
+ name: `${config.productInfo.name} Blog`,
273
+ description: config.productInfo.description,
274
+ language: "en",
275
+ websiteUrl: config.productInfo.websiteUrl,
276
+ navigation: [
277
+ { label: "Home", url: config.productInfo.websiteUrl || "/" },
278
+ { label: "Blog", url: `${config.productInfo.websiteUrl}/blog/`, active: true }
279
+ ],
280
+ primaryColor: "#2563eb",
281
+ textColor: "#1f2937",
282
+ textLight: "#6b7280",
283
+ borderColor: "#e5e7eb",
284
+ bgLight: "#f9fafb",
285
+ allowAICrawlers: true
286
+ };
287
+ for (const topic of topics.slice(0, config.count || 3)) {
288
+ try {
289
+ console.log(`
290
+ \u{1F4DD} Generating: ${topic}`);
291
+ const content = await this.generateContent(config.productInfo, topic);
292
+ const tableOfContents = this.generateTableOfContents(content);
293
+ const relatedPosts = await this.generateRelatedPosts(content, config.outputDir);
294
+ const navigation = await this.generateNavigationData(content, config.outputDir);
295
+ content.relatedPosts = relatedPosts;
296
+ const output = await adapter.render(content, {
297
+ blogConfig,
298
+ tableOfContents,
299
+ prevPost: navigation.prevPost,
300
+ nextPost: navigation.nextPost
301
+ });
302
+ const slug = content.metadata.slug;
303
+ const fileName = adapter.transformPath?.(slug) || `${slug}/index${adapter.getFileExtension()}`;
304
+ const outputPath = path2.join(config.outputDir, fileName);
305
+ if (!config.dryRun) {
306
+ await adapter.write(output, outputPath);
307
+ }
308
+ allPostsContent.push(content);
309
+ results.posts.push({
310
+ id: content.metadata.id,
311
+ title: content.metadata.title,
312
+ slug,
313
+ path: outputPath,
314
+ url: content.metadata.url,
315
+ seoScore: content.seo.score,
316
+ readTime: content.metadata.readTime,
317
+ wordCount: content.metadata.wordCount,
318
+ tags: content.metadata.tags,
319
+ keywords: content.metadata.keywords,
320
+ datePublished: content.metadata.datePublished,
321
+ seoIssues: content.seo.suggestions
322
+ });
323
+ if (!config.dryRun) {
324
+ console.log(`\u2713 Generated: ${outputPath}`);
325
+ } else {
326
+ console.log(`\u2713 Would generate: ${outputPath}`);
327
+ }
328
+ console.log(` SEO Score: ${content.seo.score}/100`);
329
+ } catch (error) {
330
+ results.errors?.push(`Failed to generate "${topic}": ${error.message}`);
331
+ results.success = false;
332
+ }
333
+ }
334
+ if (results.posts.length > 0 && allPostsContent.length > 0) {
335
+ results.manifestPath = path2.join(config.outputDir, "index.json");
336
+ results.sitemapPath = path2.join(config.outputDir, "sitemap.xml");
337
+ if (!config.dryRun) {
338
+ await this.generateManifest(config.outputDir, config.productInfo, allPostsContent);
339
+ await this.generateBlogIndex(config.outputDir, config.productInfo, blogConfig);
340
+ await this.updateSitemap(config.outputDir, config.productInfo, allPostsContent);
341
+ }
342
+ const totalWords = allPostsContent.reduce((sum, post) => sum + post.metadata.wordCount, 0);
343
+ const avgSeoScore = results.posts.reduce((sum, p) => sum + p.seoScore, 0) / results.posts.length;
344
+ const wordCounts = allPostsContent.map((post) => post.metadata.wordCount);
345
+ const avgReadTime = calculateAvgReadTime(wordCounts);
346
+ results.stats = {
347
+ avgSeoScore: Math.round(avgSeoScore * 10) / 10,
348
+ totalWords,
349
+ avgReadTime
350
+ };
351
+ }
352
+ } catch (error) {
353
+ results.success = false;
354
+ results.errors?.push(error.message);
355
+ }
356
+ return results;
357
+ }
358
+ /**
359
+ * Generate blog post content using the selected AI provider
360
+ */
361
+ async generateContent(productInfo, topic) {
362
+ const prompt = this.buildPrompt(productInfo, topic);
363
+ let contentData;
364
+ if (this.provider === "anthropic") {
365
+ contentData = await this.generateWithAnthropic(prompt);
366
+ } else if (this.provider === "openai") {
367
+ contentData = await this.generateWithOpenAI(prompt);
368
+ } else {
369
+ throw new Error(`Unsupported provider: ${this.provider}`);
370
+ }
371
+ const id = generateUUID();
372
+ const slug = slugify(contentData.metadata.title, { lower: true, strict: true });
373
+ const datePublished = generateTimestamp();
374
+ const baseUrl = productInfo.websiteUrl.replace(/\/$/, "");
375
+ const url = `${baseUrl}/blog/${slug}.html`;
376
+ const fullContent = [
377
+ contentData.content.intro,
378
+ ...contentData.content.sections.map((s) => s.content),
379
+ contentData.content.conclusion
380
+ ].join(" ");
381
+ const wordCount = calculateWordCount(fullContent);
382
+ const readTime = calculateReadTime(wordCount);
383
+ const shortDescription = extractShortDescription(contentData.content.intro);
384
+ const tags = contentData.metadata.keywords.slice(0, 5);
385
+ const content = {
386
+ metadata: {
387
+ id,
388
+ ...contentData.metadata,
389
+ slug,
390
+ url,
391
+ datePublished,
392
+ author: productInfo.name,
393
+ readTime,
394
+ wordCount,
395
+ tags,
396
+ shortDescription,
397
+ fullDescription: contentData.content.intro
398
+ },
399
+ schemaOrg: this.generateSchemaOrg({
400
+ id,
401
+ ...contentData.metadata,
402
+ slug,
403
+ url,
404
+ datePublished,
405
+ author: productInfo.name,
406
+ readTime,
407
+ wordCount,
408
+ tags
409
+ }, productInfo),
410
+ content: contentData.content,
411
+ relatedPosts: [],
412
+ seo: await this.calculateSEOScore(contentData)
413
+ };
414
+ return content;
415
+ }
416
+ /**
417
+ * Generate content using Anthropic's Claude API
418
+ */
419
+ async generateWithAnthropic(prompt) {
420
+ if (!this.anthropic) {
421
+ throw new Error("Anthropic client not initialized");
422
+ }
423
+ const response = await this.anthropic.messages.create({
424
+ model: "claude-3-5-sonnet-20241022",
425
+ max_tokens: 4096,
426
+ temperature: 0.7,
427
+ messages: [
428
+ {
429
+ role: "user",
430
+ content: prompt
431
+ }
432
+ ]
433
+ });
434
+ const textContent = response.content[0].type === "text" ? response.content[0].text : "";
435
+ const jsonMatch = textContent.match(/\{[\s\S]*\}/);
436
+ if (!jsonMatch) {
437
+ throw new Error("Failed to extract JSON from Claude response");
438
+ }
439
+ return JSON.parse(jsonMatch[0]);
440
+ }
441
+ /**
442
+ * Generate content using OpenAI's ChatGPT API
443
+ */
444
+ async generateWithOpenAI(prompt) {
445
+ if (!this.openai) {
446
+ throw new Error("OpenAI client not initialized");
447
+ }
448
+ const response = await this.openai.chat.completions.create({
449
+ model: "gpt-4-turbo-preview",
450
+ max_tokens: 4096,
451
+ temperature: 0.7,
452
+ messages: [
453
+ {
454
+ role: "user",
455
+ content: prompt
456
+ }
457
+ ],
458
+ response_format: { type: "json_object" }
459
+ });
460
+ const textContent = response.choices[0]?.message?.content || "";
461
+ if (!textContent) {
462
+ throw new Error("Failed to get response from OpenAI");
463
+ }
464
+ return JSON.parse(textContent);
465
+ }
466
+ /**
467
+ * Build prompt for Claude API
468
+ */
469
+ buildPrompt(productInfo, topic) {
470
+ return `You are an expert SEO content writer. Generate a comprehensive blog post about: "${topic}"
471
+
472
+ Product context:
473
+ - Name: ${productInfo.name}
474
+ - Tagline: ${productInfo.tagline || ""}
475
+ - Category: ${productInfo.category}
476
+ - Features: ${productInfo.features.join(", ")}
477
+ - Pricing: ${productInfo.pricingModel || "Not specified"}
478
+ ${productInfo.useCases ? `- Use cases: ${productInfo.useCases.join(", ")}` : ""}
479
+
480
+ IMPORTANT INSTRUCTIONS:
481
+ 1. Write for users searching on Google and being recommended by AI assistants (Claude, ChatGPT)
482
+ 2. Focus on being helpful, not promotional
483
+ 3. Include comparisons with alternatives when relevant
484
+ 4. Use natural language, avoid keyword stuffing
485
+ 5. Make it comprehensive (1000-1500 words worth of content)
486
+ 6. Structure with clear sections (intro, 3-5 main sections, conclusion)
487
+ 7. Be factual - do NOT invent statistics or make false claims
488
+
489
+ Output ONLY valid JSON in this exact format (no markdown, no code blocks):
490
+ {
491
+ "metadata": {
492
+ "title": "SEO-optimized title (under 60 chars)",
493
+ "description": "Meta description (under 160 chars)",
494
+ "keywords": ["keyword1", "keyword2", "keyword3"],
495
+ "category": "${productInfo.category}"
496
+ },
497
+ "content": {
498
+ "intro": "Engaging introduction paragraph in markdown format",
499
+ "sections": [
500
+ {
501
+ "heading": "Section title",
502
+ "level": 2,
503
+ "content": "Section content in markdown format",
504
+ "type": "text"
505
+ }
506
+ ],
507
+ "conclusion": "Conclusion paragraph in markdown",
508
+ "cta": {
509
+ "text": "Try ${productInfo.name}",
510
+ "url": "${productInfo.websiteUrl || "/signup"}"
511
+ }
512
+ }
513
+ }`;
514
+ }
515
+ /**
516
+ * Generate Schema.org structured data
517
+ */
518
+ generateSchemaOrg(metadata, productInfo) {
519
+ return {
520
+ "@context": "https://schema.org",
521
+ "@type": "BlogPosting",
522
+ headline: metadata.title,
523
+ description: metadata.description,
524
+ author: {
525
+ "@type": "Organization",
526
+ name: productInfo.name,
527
+ url: productInfo.websiteUrl
528
+ },
529
+ datePublished: metadata.datePublished,
530
+ publisher: {
531
+ "@type": "Organization",
532
+ name: productInfo.name
533
+ },
534
+ keywords: metadata.keywords.join(", "),
535
+ articleSection: metadata.category
536
+ };
537
+ }
538
+ /**
539
+ * Calculate SEO score
540
+ */
541
+ async calculateSEOScore(content) {
542
+ let score = 100;
543
+ const suggestions = [];
544
+ if (content.metadata.title.length > 60) {
545
+ score -= 10;
546
+ suggestions.push("Title is too long (should be under 60 chars)");
547
+ }
548
+ if (content.metadata.description.length > 160) {
549
+ score -= 10;
550
+ suggestions.push("Meta description is too long (should be under 160 chars)");
551
+ }
552
+ if (content.metadata.keywords.length < 3) {
553
+ score -= 5;
554
+ suggestions.push("Add more keywords (at least 3)");
555
+ }
556
+ if (content.content.sections.length < 3) {
557
+ score -= 10;
558
+ suggestions.push("Add more sections for better structure (at least 3)");
559
+ }
560
+ return {
561
+ score: Math.max(0, score),
562
+ suggestions
563
+ };
564
+ }
565
+ /**
566
+ * Generate topic suggestions
567
+ */
568
+ async generateTopics(productInfo, count) {
569
+ const prompt = `Generate ${count} SEO-friendly blog post topics for a product called "${productInfo.name}" in the ${productInfo.category} category.
570
+
571
+ Product description: ${productInfo.description}
572
+ Features: ${productInfo.features.join(", ")}
573
+
574
+ Requirements:
575
+ - Topics should be helpful for potential customers
576
+ - Focus on solving problems or answering questions
577
+ - Include comparisons, guides, and use cases
578
+ - Optimize for search engines and AI recommendations
579
+
580
+ Output ONLY a JSON array of topic strings, no markdown:
581
+ ["Topic 1", "Topic 2", "Topic 3"]`;
582
+ try {
583
+ if (this.provider === "anthropic" && this.anthropic) {
584
+ const response = await this.anthropic.messages.create({
585
+ model: "claude-3-5-sonnet-20241022",
586
+ max_tokens: 1024,
587
+ messages: [{ role: "user", content: prompt }]
588
+ });
589
+ const textContent = response.content[0].type === "text" ? response.content[0].text : "";
590
+ const jsonMatch = textContent.match(/\[[\s\S]*\]/);
591
+ if (jsonMatch) {
592
+ return JSON.parse(jsonMatch[0]);
593
+ }
594
+ } else if (this.provider === "openai" && this.openai) {
595
+ const response = await this.openai.chat.completions.create({
596
+ model: "gpt-4-turbo-preview",
597
+ max_tokens: 1024,
598
+ messages: [{ role: "user", content: prompt }],
599
+ response_format: { type: "json_object" }
600
+ });
601
+ const textContent = response.choices[0]?.message?.content || "";
602
+ if (textContent) {
603
+ const data = JSON.parse(textContent);
604
+ const topicsArray = data.topics || data;
605
+ if (Array.isArray(topicsArray)) {
606
+ return topicsArray;
607
+ }
608
+ if (typeof topicsArray === "object") {
609
+ return Object.values(topicsArray).filter((v) => typeof v === "string");
610
+ }
611
+ }
612
+ }
613
+ } catch (error) {
614
+ console.warn("Failed to generate topics with AI, using fallback");
615
+ }
616
+ return [
617
+ `Best ${productInfo.category} software ${(/* @__PURE__ */ new Date()).getFullYear()}`,
618
+ `How to choose ${productInfo.category} solution`,
619
+ `${productInfo.name} vs alternatives: Complete comparison`
620
+ ];
621
+ }
622
+ /**
623
+ * Generate blog manifest (index.json)
624
+ */
625
+ async generateManifest(outputDir, productInfo, posts) {
626
+ const { calculateAvgReadTime } = await import("./content-helpers-TXVEJMQK.js");
627
+ const manifestPosts = posts.map((post) => ({
628
+ id: post.metadata.id,
629
+ slug: post.metadata.slug,
630
+ url: post.metadata.url,
631
+ title: post.metadata.title,
632
+ shortDescription: post.metadata.shortDescription || post.metadata.description,
633
+ fullDescription: post.metadata.fullDescription || post.content.intro,
634
+ publishDate: post.metadata.datePublished,
635
+ readTime: post.metadata.readTime,
636
+ wordCount: post.metadata.wordCount,
637
+ tags: post.metadata.tags,
638
+ category: post.metadata.category,
639
+ seo: {
640
+ metaTitle: post.metadata.title,
641
+ metaDescription: post.metadata.description,
642
+ keywords: post.metadata.keywords,
643
+ seoScore: post.seo.score
644
+ }
645
+ }));
646
+ const totalWords = posts.reduce((sum, post) => sum + post.metadata.wordCount, 0);
647
+ const wordCounts = posts.map((post) => post.metadata.wordCount);
648
+ const avgReadTime = calculateAvgReadTime(wordCounts);
649
+ const manifest = {
650
+ version: "1.0.0",
651
+ generated: generateTimestamp(),
652
+ site: {
653
+ name: `${productInfo.name} Blog`,
654
+ url: productInfo.websiteUrl + "/blog",
655
+ description: productInfo.description || `Insights on ${productInfo.category}`
656
+ },
657
+ posts: manifestPosts,
658
+ stats: {
659
+ totalPosts: posts.length,
660
+ totalWords,
661
+ avgReadTime
662
+ }
663
+ };
664
+ const manifestPath = path2.join(outputDir, "index.json");
665
+ await fs2.writeJson(manifestPath, manifest, { spaces: 2 });
666
+ console.log(`\u2713 Generated blog manifest: ${manifestPath}`);
667
+ }
668
+ /**
669
+ * Generate blog index.html page
670
+ */
671
+ async generateBlogIndex(outputDir, productInfo, blogConfig) {
672
+ const ejs2 = await import("ejs");
673
+ const possiblePaths = [
674
+ path2.join(path2.dirname(fileURLToPath2(import.meta.url)), "../adapters/html/templates/blog-index.html.ejs"),
675
+ path2.join(path2.dirname(fileURLToPath2(import.meta.url)), "../../adapters/html/templates/blog-index.html.ejs"),
676
+ path2.join(path2.dirname(fileURLToPath2(import.meta.url)), "adapters/html/templates/blog-index.html.ejs")
677
+ ];
678
+ let templatePath = null;
679
+ for (const p of possiblePaths) {
680
+ if (await fs2.pathExists(p)) {
681
+ templatePath = p;
682
+ break;
683
+ }
684
+ }
685
+ if (!templatePath) {
686
+ console.warn(`Blog index template not found in any of the expected locations. Skipping blog index generation.`);
687
+ return;
688
+ }
689
+ const template = await fs2.readFile(templatePath, "utf-8");
690
+ const html = ejs2.render(template, {
691
+ siteName: `${productInfo.name} Blog`,
692
+ siteDescription: productInfo.description || `Insights on ${productInfo.category}`,
693
+ siteUrl: productInfo.websiteUrl + "/blog",
694
+ blogConfig
695
+ });
696
+ const indexPath = path2.join(outputDir, "index.html");
697
+ await fs2.writeFile(indexPath, html, "utf-8");
698
+ console.log(`\u2713 Generated blog index: ${indexPath}`);
699
+ }
700
+ /**
701
+ * Update sitemap.xml
702
+ */
703
+ async updateSitemap(blogDir, productInfo, posts) {
704
+ const sitemapPath = path2.join(blogDir, "sitemap.xml");
705
+ const urls = posts.map((post) => {
706
+ return ` <url>
707
+ <loc>${post.metadata.url}</loc>
708
+ <lastmod>${post.metadata.datePublished.split("T")[0]}</lastmod>
709
+ <changefreq>weekly</changefreq>
710
+ <priority>0.8</priority>
711
+ </url>`;
712
+ }).join("\n");
713
+ const baseUrl = productInfo.websiteUrl.replace(/\/$/, "");
714
+ const blogIndexUrl = ` <url>
715
+ <loc>${baseUrl}/blog/</loc>
716
+ <lastmod>${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}</lastmod>
717
+ <changefreq>daily</changefreq>
718
+ <priority>1.0</priority>
719
+ </url>`;
720
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
721
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
722
+ ${blogIndexUrl}
723
+ ${urls}
724
+ </urlset>`;
725
+ await fs2.writeFile(sitemapPath, sitemap, "utf-8");
726
+ console.log(`\u2713 Generated sitemap: ${sitemapPath}`);
727
+ }
728
+ /**
729
+ * Generate table of contents from post sections
730
+ */
731
+ generateTableOfContents(content) {
732
+ const toc = [];
733
+ content.content.sections.forEach((section) => {
734
+ if (section.level >= 2 && section.level <= 4) {
735
+ const id = section.heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
736
+ toc.push({
737
+ id,
738
+ text: section.heading,
739
+ level: section.level
740
+ });
741
+ }
742
+ });
743
+ return toc;
744
+ }
745
+ /**
746
+ * Find related posts based on tags and category similarity
747
+ */
748
+ async generateRelatedPosts(currentPost, outputDir, limit = 3) {
749
+ try {
750
+ const manifestPath = path2.join(outputDir, "index.json");
751
+ if (!await fs2.pathExists(manifestPath)) {
752
+ return [];
753
+ }
754
+ const manifest = await fs2.readJson(manifestPath);
755
+ const scoredPosts = manifest.posts.filter((post) => post.id !== currentPost.metadata.id).map((post) => {
756
+ let score = 0;
757
+ if (post.category === currentPost.metadata.category) {
758
+ score += 3;
759
+ }
760
+ const sharedTags = post.tags.filter(
761
+ (tag) => currentPost.metadata.tags.includes(tag)
762
+ );
763
+ score += sharedTags.length;
764
+ return {
765
+ post,
766
+ score
767
+ };
768
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
769
+ return scoredPosts.map((item) => ({
770
+ title: item.post.title,
771
+ slug: item.post.slug,
772
+ url: item.post.url,
773
+ excerpt: item.post.shortDescription
774
+ }));
775
+ } catch (error) {
776
+ console.warn("Failed to generate related posts:", error);
777
+ return [];
778
+ }
779
+ }
780
+ /**
781
+ * Get previous and next posts for navigation
782
+ */
783
+ async generateNavigationData(currentPost, outputDir) {
784
+ try {
785
+ const manifestPath = path2.join(outputDir, "index.json");
786
+ if (!await fs2.pathExists(manifestPath)) {
787
+ return {};
788
+ }
789
+ const manifest = await fs2.readJson(manifestPath);
790
+ const sortedPosts = [...manifest.posts].sort(
791
+ (a, b) => new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime()
792
+ );
793
+ const currentIndex = sortedPosts.findIndex(
794
+ (post) => post.id === currentPost.metadata.id
795
+ );
796
+ if (currentIndex === -1) {
797
+ return {};
798
+ }
799
+ const prevPost = sortedPosts[currentIndex + 1];
800
+ const nextPost = sortedPosts[currentIndex - 1];
801
+ return {
802
+ prevPost: prevPost ? {
803
+ title: prevPost.title,
804
+ url: prevPost.url,
805
+ slug: prevPost.slug
806
+ } : void 0,
807
+ nextPost: nextPost ? {
808
+ title: nextPost.title,
809
+ url: nextPost.url,
810
+ slug: nextPost.slug
811
+ } : void 0
812
+ };
813
+ } catch (error) {
814
+ console.warn("Failed to generate navigation data:", error);
815
+ return {};
816
+ }
817
+ }
818
+ };
819
+
820
+ export {
821
+ AdapterRegistry,
822
+ HTMLAdapter,
823
+ ContentGenerator
824
+ };