@knowcode/doc-builder 1.4.25 → 1.5.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/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',
@@ -92,6 +99,27 @@ async function setupVercelProject(config) {
92
99
  fs.writeJsonSync(vercelConfigPath, vercelConfig, { spaces: 2 });
93
100
  console.log(chalk.green(`✅ Created vercel.json in ${config.outputDir || 'html'} directory`));
94
101
 
102
+ // Save production URL to config if provided
103
+ if (answers.productionUrl) {
104
+ const configPath = path.join(process.cwd(), 'doc-builder.config.js');
105
+ if (fs.existsSync(configPath)) {
106
+ try {
107
+ let configContent = fs.readFileSync(configPath, 'utf8');
108
+ // Add productionUrl to the config
109
+ if (!configContent.includes('productionUrl:')) {
110
+ configContent = configContent.replace(
111
+ /module\.exports = {/,
112
+ `module.exports = {\n productionUrl: '${answers.productionUrl.startsWith('http') ? answers.productionUrl : 'https://' + answers.productionUrl}',`
113
+ );
114
+ fs.writeFileSync(configPath, configContent);
115
+ console.log(chalk.green(`✅ Saved production URL to config: ${answers.productionUrl}`));
116
+ }
117
+ } catch (e) {
118
+ console.log(chalk.yellow('⚠️ Could not save production URL to config file'));
119
+ }
120
+ }
121
+ }
122
+
95
123
  // Run Vercel setup with prominent instructions
96
124
  console.log(chalk.blue('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
97
125
  console.log(chalk.blue('🔗 Linking to Vercel - IMPORTANT INSTRUCTIONS'));
@@ -112,8 +140,13 @@ async function setupVercelProject(config) {
112
140
  console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold('YES') + ' if you have an existing project');
113
141
  console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold('NO') + ' to create new project\n');
114
142
 
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');
143
+ console.log(chalk.white('5️⃣ ') + chalk.yellow('If you answered YES to #4:'));
144
+ console.log(chalk.white(' ') + chalk.green('What\'s the name of your existing project?'));
145
+ console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold(answers.projectName) + ' (your existing project name)\n');
146
+
147
+ console.log(chalk.white('5️⃣ ') + chalk.yellow('If you answered NO to #4:'));
148
+ console.log(chalk.white(' ') + chalk.green('What is your project name?'));
149
+ console.log(chalk.white(' 👉 Answer: ') + chalk.yellow.bold(answers.projectName) + ' (e.g., "my-docs" or "gasworld")\n');
117
150
 
118
151
  console.log(chalk.red.bold('⚠️ CRITICAL WARNING ABOUT ROOT DIRECTORY:\n'));
119
152
  console.log(chalk.bgRed.white.bold(' If Vercel asks about Root Directory or shows it in settings: '));
@@ -285,6 +318,12 @@ async function deployToVercel(config, isProd = false) {
285
318
  // Try to get the production URL from Vercel
286
319
  let productionUrl = null;
287
320
 
321
+ // First, check if we have a configured production URL
322
+ if (config.productionUrl) {
323
+ productionUrl = config.productionUrl;
324
+ console.log(chalk.blue('\n📌 Using configured production URL'));
325
+ }
326
+
288
327
  try {
289
328
  const { execSync } = require('child_process');
290
329
 
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.0",
4
4
  "description": "Reusable documentation builder for markdown-based sites with Vercel deployment support",
5
5
  "main": "index.js",
6
6
  "bin": {