@knowcode/doc-builder 1.4.25 → 1.5.1

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/lib/deploy.js CHANGED
@@ -28,6 +28,13 @@ async function setupVercelProject(config) {
28
28
  initial: config.siteName.toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'my-docs',
29
29
  hint: 'This will be your URL: project-name.vercel.app'
30
30
  },
31
+ {
32
+ type: 'text',
33
+ name: 'productionUrl',
34
+ message: 'Custom production URL (optional)?',
35
+ initial: '',
36
+ hint: 'Leave empty for auto-detection, or enter your custom domain/alias (e.g., doc-builder-delta.vercel.app)'
37
+ },
31
38
  {
32
39
  type: 'select',
33
40
  name: 'framework',
@@ -86,12 +93,43 @@ async function setupVercelProject(config) {
86
93
  }
87
94
  ]
88
95
  }
96
+ ],
97
+ "routes": [
98
+ {
99
+ "handle": "filesystem"
100
+ },
101
+ {
102
+ "src": "/(.*)",
103
+ "status": 404,
104
+ "dest": "/404.html"
105
+ }
89
106
  ]
90
107
  };
91
108
 
92
109
  fs.writeJsonSync(vercelConfigPath, vercelConfig, { spaces: 2 });
93
110
  console.log(chalk.green(`✅ Created vercel.json in ${config.outputDir || 'html'} directory`));
94
111
 
112
+ // Save production URL to config if provided
113
+ if (answers.productionUrl) {
114
+ const configPath = path.join(process.cwd(), 'doc-builder.config.js');
115
+ if (fs.existsSync(configPath)) {
116
+ try {
117
+ let configContent = fs.readFileSync(configPath, 'utf8');
118
+ // Add productionUrl to the config
119
+ if (!configContent.includes('productionUrl:')) {
120
+ configContent = configContent.replace(
121
+ /module\.exports = {/,
122
+ `module.exports = {\n productionUrl: '${answers.productionUrl.startsWith('http') ? answers.productionUrl : 'https://' + answers.productionUrl}',`
123
+ );
124
+ fs.writeFileSync(configPath, configContent);
125
+ console.log(chalk.green(`✅ Saved production URL to config: ${answers.productionUrl}`));
126
+ }
127
+ } catch (e) {
128
+ console.log(chalk.yellow('⚠️ Could not save production URL to config file'));
129
+ }
130
+ }
131
+ }
132
+
95
133
  // Run Vercel setup with prominent instructions
96
134
  console.log(chalk.blue('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
97
135
  console.log(chalk.blue('🔗 Linking to Vercel - IMPORTANT INSTRUCTIONS'));
@@ -112,8 +150,13 @@ async function setupVercelProject(config) {
112
150
  console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold('YES') + ' if you have an existing project');
113
151
  console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold('NO') + ' to create new project\n');
114
152
 
115
- console.log(chalk.white('5️⃣ ') + chalk.green('What\'s the name of your existing project?'));
116
- console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold(answers.projectName) + ' (your actual project name)\n');
153
+ console.log(chalk.white('5️⃣ ') + chalk.yellow('If you answered YES to #4:'));
154
+ console.log(chalk.white(' ') + chalk.green('What\'s the name of your existing project?'));
155
+ console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold(answers.projectName) + ' (your existing project name)\n');
156
+
157
+ console.log(chalk.white('5️⃣ ') + chalk.yellow('If you answered NO to #4:'));
158
+ console.log(chalk.white(' ') + chalk.green('What is your project name?'));
159
+ console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold(answers.projectName) + ' (e.g., "my-docs" or "gasworld")\n');
117
160
 
118
161
  console.log(chalk.red.bold('⚠️ CRITICAL WARNING ABOUT ROOT DIRECTORY:\n'));
119
162
  console.log(chalk.bgRed.white.bold(' If Vercel asks about Root Directory or shows it in settings: '));
@@ -232,6 +275,16 @@ async function deployToVercel(config, isProd = false) {
232
275
  }
233
276
  ]
234
277
  }
278
+ ],
279
+ "routes": [
280
+ {
281
+ "handle": "filesystem"
282
+ },
283
+ {
284
+ "src": "/(.*)",
285
+ "status": 404,
286
+ "dest": "/404.html"
287
+ }
235
288
  ]
236
289
  };
237
290
  fs.writeJsonSync(vercelConfigPath, vercelConfig, { spaces: 2 });
@@ -285,6 +338,12 @@ async function deployToVercel(config, isProd = false) {
285
338
  // Try to get the production URL from Vercel
286
339
  let productionUrl = null;
287
340
 
341
+ // First, check if we have a configured production URL
342
+ if (config.productionUrl) {
343
+ productionUrl = config.productionUrl;
344
+ console.log(chalk.blue('\n📌 Using configured production URL'));
345
+ }
346
+
288
347
  try {
289
348
  const { execSync } = require('child_process');
290
349
 
package/lib/seo.js ADDED
@@ -0,0 +1,359 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * SEO Utilities for @knowcode/doc-builder
7
+ * Handles meta tags, structured data, sitemaps, and robots.txt
8
+ */
9
+
10
+ /**
11
+ * Extract keywords from markdown content
12
+ */
13
+ function extractKeywords(content, maxKeywords = 10) {
14
+ // Remove markdown syntax
15
+ const plainText = content
16
+ .replace(/```[\s\S]*?```/g, '') // Remove code blocks
17
+ .replace(/`[^`]*`/g, '') // Remove inline code
18
+ .replace(/#+\s/g, '') // Remove headers
19
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Extract link text
20
+ .replace(/[*_~]/g, '') // Remove emphasis
21
+ .toLowerCase();
22
+
23
+ // Common words to exclude
24
+ const stopWords = new Set([
25
+ 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but',
26
+ 'in', 'with', 'to', 'for', 'of', 'as', 'by', 'that', 'this',
27
+ 'it', 'from', 'be', 'are', 'been', 'was', 'were', 'being'
28
+ ]);
29
+
30
+ // Extract words
31
+ const words = plainText.match(/\b[a-z]{3,}\b/g) || [];
32
+
33
+ // Count word frequency
34
+ const wordCount = {};
35
+ words.forEach(word => {
36
+ if (!stopWords.has(word)) {
37
+ wordCount[word] = (wordCount[word] || 0) + 1;
38
+ }
39
+ });
40
+
41
+ // Sort by frequency and return top keywords
42
+ return Object.entries(wordCount)
43
+ .sort((a, b) => b[1] - a[1])
44
+ .slice(0, maxKeywords)
45
+ .map(([word]) => word);
46
+ }
47
+
48
+ /**
49
+ * Generate meta description from content
50
+ */
51
+ function generateDescription(content, maxLength = 160) {
52
+ // Remove markdown syntax and get first paragraph
53
+ const plainText = content
54
+ .replace(/```[\s\S]*?```/g, '') // Remove code blocks
55
+ .replace(/#+\s(.+)/g, '$1. ') // Convert headers to sentences
56
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Extract link text
57
+ .replace(/[*_~]/g, '') // Remove emphasis
58
+ .replace(/\n+/g, ' ') // Replace newlines with spaces
59
+ .trim();
60
+
61
+ // Find first sentence or use first maxLength characters
62
+ const firstSentence = plainText.match(/^[^.!?]+[.!?]/);
63
+ let description = firstSentence ? firstSentence[0] : plainText;
64
+
65
+ // Truncate if needed
66
+ if (description.length > maxLength) {
67
+ description = description.substring(0, maxLength - 3) + '...';
68
+ }
69
+
70
+ return description;
71
+ }
72
+
73
+ /**
74
+ * Generate meta tags for a page
75
+ */
76
+ function generateMetaTags(options) {
77
+ const {
78
+ title,
79
+ description,
80
+ url,
81
+ author,
82
+ keywords,
83
+ twitterHandle,
84
+ ogImage,
85
+ siteName,
86
+ language = 'en-US',
87
+ type = 'article'
88
+ } = options;
89
+
90
+ const tags = [];
91
+
92
+ // Basic meta tags
93
+ if (author) {
94
+ tags.push(` <meta name="author" content="${escapeHtml(author)}">`);
95
+ }
96
+
97
+ if (keywords && keywords.length > 0) {
98
+ const keywordString = Array.isArray(keywords) ? keywords.join(', ') : keywords;
99
+ tags.push(` <meta name="keywords" content="${escapeHtml(keywordString)}">`);
100
+ }
101
+
102
+ tags.push(` <meta name="robots" content="index, follow">`);
103
+
104
+ if (url) {
105
+ tags.push(` <link rel="canonical" href="${url}">`);
106
+ }
107
+
108
+ // Open Graph tags
109
+ tags.push(` \n <!-- Open Graph / Facebook -->`);
110
+ tags.push(` <meta property="og:type" content="${type}">`);
111
+
112
+ if (url) {
113
+ tags.push(` <meta property="og:url" content="${url}">`);
114
+ }
115
+
116
+ if (title) {
117
+ tags.push(` <meta property="og:title" content="${escapeHtml(title)}">`);
118
+ }
119
+
120
+ if (description) {
121
+ tags.push(` <meta property="og:description" content="${escapeHtml(description)}">`);
122
+ }
123
+
124
+ if (ogImage) {
125
+ const imageUrl = ogImage.startsWith('http') ? ogImage : (url ? new URL(ogImage, url).href : ogImage);
126
+ tags.push(` <meta property="og:image" content="${imageUrl}">`);
127
+ }
128
+
129
+ if (siteName) {
130
+ tags.push(` <meta property="og:site_name" content="${escapeHtml(siteName)}">`);
131
+ }
132
+
133
+ tags.push(` <meta property="og:locale" content="${language.replace('-', '_')}">`);
134
+
135
+ // Twitter Card tags
136
+ if (twitterHandle || ogImage) {
137
+ tags.push(` \n <!-- Twitter Card -->`);
138
+ tags.push(` <meta name="twitter:card" content="summary_large_image">`);
139
+
140
+ if (twitterHandle) {
141
+ const handle = twitterHandle.startsWith('@') ? twitterHandle : '@' + twitterHandle;
142
+ tags.push(` <meta name="twitter:site" content="${handle}">`);
143
+ tags.push(` <meta name="twitter:creator" content="${handle}">`);
144
+ }
145
+
146
+ if (title) {
147
+ tags.push(` <meta name="twitter:title" content="${escapeHtml(title)}">`);
148
+ }
149
+
150
+ if (description) {
151
+ tags.push(` <meta name="twitter:description" content="${escapeHtml(description)}">`);
152
+ }
153
+
154
+ if (ogImage) {
155
+ const imageUrl = ogImage.startsWith('http') ? ogImage : (url ? new URL(ogImage, url).href : ogImage);
156
+ tags.push(` <meta name="twitter:image" content="${imageUrl}">`);
157
+ }
158
+ }
159
+
160
+ return tags.join('\n');
161
+ }
162
+
163
+ /**
164
+ * Generate JSON-LD structured data
165
+ */
166
+ function generateJSONLD(options) {
167
+ const {
168
+ title,
169
+ description,
170
+ url,
171
+ author,
172
+ siteName,
173
+ datePublished,
174
+ dateModified,
175
+ breadcrumbs,
176
+ organization,
177
+ type = 'TechArticle'
178
+ } = options;
179
+
180
+ const jsonld = {
181
+ '@context': 'https://schema.org',
182
+ '@type': type,
183
+ headline: title,
184
+ description: description
185
+ };
186
+
187
+ if (author) {
188
+ jsonld.author = {
189
+ '@type': 'Person',
190
+ name: author
191
+ };
192
+ }
193
+
194
+ if (organization) {
195
+ jsonld.publisher = {
196
+ '@type': 'Organization',
197
+ name: organization.name,
198
+ url: organization.url
199
+ };
200
+
201
+ if (organization.logo) {
202
+ jsonld.publisher.logo = {
203
+ '@type': 'ImageObject',
204
+ url: organization.logo
205
+ };
206
+ }
207
+ }
208
+
209
+ if (datePublished) {
210
+ jsonld.datePublished = datePublished;
211
+ }
212
+
213
+ if (dateModified) {
214
+ jsonld.dateModified = dateModified;
215
+ }
216
+
217
+ if (url) {
218
+ jsonld.mainEntityOfPage = {
219
+ '@type': 'WebPage',
220
+ '@id': url
221
+ };
222
+ }
223
+
224
+ // Add breadcrumb if provided
225
+ if (breadcrumbs && breadcrumbs.length > 0) {
226
+ jsonld.breadcrumb = {
227
+ '@type': 'BreadcrumbList',
228
+ itemListElement: breadcrumbs.map((crumb, index) => ({
229
+ '@type': 'ListItem',
230
+ position: index + 1,
231
+ name: crumb.name,
232
+ item: crumb.url
233
+ }))
234
+ };
235
+ }
236
+
237
+ return `<script type="application/ld+json">\n${JSON.stringify(jsonld, null, 2)}\n</script>`;
238
+ }
239
+
240
+ /**
241
+ * Generate sitemap.xml
242
+ */
243
+ async function generateSitemap(pages, siteUrl, outputDir) {
244
+ console.log(chalk.blue('🗺️ Generating sitemap.xml...'));
245
+
246
+ const sitemap = [
247
+ '<?xml version="1.0" encoding="UTF-8"?>',
248
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
249
+ ];
250
+
251
+ const now = new Date().toISOString();
252
+
253
+ pages.forEach(page => {
254
+ const priority = page.path === 'index.html' ? '1.0' :
255
+ page.path.includes('guide') ? '0.8' : '0.6';
256
+ const changefreq = page.path === 'index.html' ? 'weekly' : 'monthly';
257
+
258
+ sitemap.push(' <url>');
259
+ sitemap.push(` <loc>${siteUrl}/${page.path}</loc>`);
260
+ sitemap.push(` <lastmod>${now}</lastmod>`);
261
+ sitemap.push(` <changefreq>${changefreq}</changefreq>`);
262
+ sitemap.push(` <priority>${priority}</priority>`);
263
+ sitemap.push(' </url>');
264
+ });
265
+
266
+ sitemap.push('</urlset>');
267
+
268
+ const sitemapPath = path.join(outputDir, 'sitemap.xml');
269
+ await fs.writeFile(sitemapPath, sitemap.join('\n'));
270
+ console.log(chalk.green(`✅ Generated sitemap.xml with ${pages.length} URLs`));
271
+
272
+ return sitemapPath;
273
+ }
274
+
275
+ /**
276
+ * Generate robots.txt
277
+ */
278
+ async function generateRobotsTxt(siteUrl, outputDir, options = {}) {
279
+ console.log(chalk.blue('🤖 Generating robots.txt...'));
280
+
281
+ const robots = [
282
+ 'User-agent: *',
283
+ 'Allow: /',
284
+ '',
285
+ '# Sitemap location',
286
+ `Sitemap: ${siteUrl}/sitemap.xml`
287
+ ];
288
+
289
+ // Add disallow for auth pages if authentication is enabled
290
+ if (options.hasAuthentication) {
291
+ robots.push('', '# Authentication pages');
292
+ robots.push('Disallow: /login.html');
293
+ robots.push('Disallow: /logout.html');
294
+ }
295
+
296
+ // Add custom rules if provided
297
+ if (options.customRules) {
298
+ robots.push('', '# Custom rules');
299
+ robots.push(...options.customRules);
300
+ }
301
+
302
+ robots.push('');
303
+
304
+ const robotsPath = path.join(outputDir, 'robots.txt');
305
+ await fs.writeFile(robotsPath, robots.join('\n'));
306
+ console.log(chalk.green('✅ Generated robots.txt'));
307
+
308
+ return robotsPath;
309
+ }
310
+
311
+ /**
312
+ * Escape HTML special characters
313
+ */
314
+ function escapeHtml(str) {
315
+ if (!str) return '';
316
+ return str
317
+ .replace(/&/g, '&amp;')
318
+ .replace(/</g, '&lt;')
319
+ .replace(/>/g, '&gt;')
320
+ .replace(/"/g, '&quot;')
321
+ .replace(/'/g, '&#39;');
322
+ }
323
+
324
+ /**
325
+ * Generate breadcrumbs from file path
326
+ */
327
+ function generateBreadcrumbs(filePath, siteUrl, siteName) {
328
+ const parts = filePath.split('/').filter(p => p && p !== 'index.html');
329
+ const breadcrumbs = [
330
+ { name: siteName || 'Home', url: siteUrl }
331
+ ];
332
+
333
+ let currentPath = '';
334
+ parts.forEach((part, index) => {
335
+ currentPath += (currentPath ? '/' : '') + part;
336
+ const name = part
337
+ .replace('.html', '')
338
+ .replace(/-/g, ' ')
339
+ .replace(/\b\w/g, l => l.toUpperCase());
340
+
341
+ breadcrumbs.push({
342
+ name: name,
343
+ url: `${siteUrl}/${currentPath}${part.endsWith('.html') ? '' : '/'}`
344
+ });
345
+ });
346
+
347
+ return breadcrumbs;
348
+ }
349
+
350
+ module.exports = {
351
+ extractKeywords,
352
+ generateDescription,
353
+ generateMetaTags,
354
+ generateJSONLD,
355
+ generateSitemap,
356
+ generateRobotsTxt,
357
+ generateBreadcrumbs,
358
+ escapeHtml
359
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowcode/doc-builder",
3
- "version": "1.4.25",
3
+ "version": "1.5.1",
4
4
  "description": "Reusable documentation builder for markdown-based sites with Vercel deployment support",
5
5
  "main": "index.js",
6
6
  "bin": {