@knowcode/doc-builder 1.4.24 → 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: '));
@@ -282,55 +315,79 @@ async function deployToVercel(config, isProd = false) {
282
315
 
283
316
  vercelProcess.on('close', async (code) => {
284
317
  if (code === 0) {
285
- // Try to get the production URL from Vercel CLI
318
+ // Try to get the production URL from Vercel
319
+ let productionUrl = null;
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
+
286
327
  try {
287
328
  const { execSync } = require('child_process');
288
- const projectInfo = execSync('vercel project ls', {
289
- cwd: outputPath,
290
- encoding: 'utf8',
291
- stdio: ['pipe', 'pipe', 'ignore']
292
- });
293
-
294
- // Try to determine the project name from the deployment URL or .vercel/project.json
295
- let projectName = null;
296
329
 
297
- // First try to get from .vercel/project.json
330
+ // Method 1: Check for project domains
298
331
  try {
299
- const projectJsonPath = path.join(outputPath, '.vercel', 'project.json');
300
- if (fs.existsSync(projectJsonPath)) {
301
- const projectData = fs.readJsonSync(projectJsonPath);
302
- // Get project name from Vercel API if needed
303
- const projectId = projectData.projectId;
304
- if (projectId) {
305
- // For now, try to extract from deployment URL
306
- const deployMatch = deployUrl.match(/https:\/\/([^-]+)/);
307
- if (deployMatch) {
308
- projectName = deployMatch[1];
332
+ const domainsOutput = execSync('vercel domains ls', {
333
+ cwd: outputPath,
334
+ encoding: 'utf8',
335
+ stdio: ['pipe', 'pipe', 'ignore']
336
+ });
337
+
338
+ // Look for domains associated with this project
339
+ const domainLines = domainsOutput.split('\n');
340
+ for (const line of domainLines) {
341
+ if (line.includes('.vercel.app') && !line.includes('source')) {
342
+ // Extract domain that looks like a custom project domain
343
+ const match = line.match(/([a-z0-9-]+\.vercel\.app)/);
344
+ if (match && !match[1].includes('lindsay-1340s-projects')) {
345
+ productionUrl = `https://${match[1]}`;
346
+ break;
309
347
  }
310
348
  }
311
349
  }
312
350
  } catch (e) {
313
- // Ignore errors reading project.json
351
+ // domains command might not be available
352
+ }
353
+
354
+ // Method 2: Get project info and construct standard production URL
355
+ if (!productionUrl) {
356
+ try {
357
+ // Get the project name from the deployment
358
+ const inspectOutput = execSync(`vercel inspect ${deployUrl}`, {
359
+ cwd: outputPath,
360
+ encoding: 'utf8',
361
+ stdio: ['pipe', 'pipe', 'ignore']
362
+ });
363
+
364
+ // Extract project name from inspect output
365
+ const projectMatch = inspectOutput.match(/Project Name:\s+([^\s]+)/);
366
+ if (projectMatch) {
367
+ const projectName = projectMatch[1];
368
+ // Construct the standard production URL
369
+ productionUrl = `https://${projectName}.vercel.app`;
370
+ }
371
+ } catch (e) {
372
+ // inspect command failed
373
+ }
314
374
  }
315
375
 
316
- // Extract the production URL for the current project
317
- const lines = projectInfo.split('\n');
318
- for (const line of lines) {
319
- // Look for project by checking if the line contains the deployment URL domain
320
- const urlMatch = line.match(/https:\/\/[^\s]+\.vercel\.app/);
321
- if (urlMatch && (
322
- (projectName && line.toLowerCase().includes(projectName)) ||
323
- line.includes(deployUrl.split('.')[0].split('//')[1])
324
- )) {
325
- resolve({ deployUrl, productionUrl: urlMatch[0] });
326
- return;
376
+ // Method 3: Extract from deployment URL pattern
377
+ if (!productionUrl && deployUrl) {
378
+ // Try to extract project name from deployment URL
379
+ // Format: https://project-name-randomhash-team.vercel.app
380
+ const urlMatch = deployUrl.match(/https:\/\/([^-]+)-[a-z0-9]+-/);
381
+ if (urlMatch) {
382
+ const projectName = urlMatch[1];
383
+ productionUrl = `https://${projectName}.vercel.app`;
327
384
  }
328
385
  }
329
386
  } catch (err) {
330
- // Fallback if we can't get project info
387
+ // All methods failed, use deployment URL
331
388
  }
332
389
 
333
- resolve({ deployUrl, productionUrl: null });
390
+ resolve({ deployUrl, productionUrl });
334
391
  } else {
335
392
  reject(new Error(`Vercel deployment failed with code ${code}`));
336
393
  }
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.24",
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": {