@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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +15 -0
- package/README.md +57 -7
- package/cli.js +251 -2
- package/doc-builder.config.js +38 -0
- package/html/README.html +71 -5
- package/html/claude-workflow-guide.html +71 -5
- package/html/documentation-index.html +77 -5
- package/html/guides/authentication-guide.html +77 -5
- package/html/guides/documentation-standards.html +77 -5
- package/html/guides/seo-guide.html +557 -0
- package/html/guides/troubleshooting-guide.html +77 -5
- package/html/index.html +71 -5
- package/html/robots.txt +5 -0
- package/html/sitemap.xml +69 -0
- package/html/vercel-cli-setup-guide.html +452 -0
- package/html/vercel-first-time-setup-guide.html +419 -0
- package/lib/config.js +39 -1
- package/lib/core-builder.js +103 -5
- package/lib/deploy.js +41 -2
- package/lib/seo.js +359 -0
- package/package.json +1 -1
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.
|
|
116
|
-
console.log(chalk.white('
|
|
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, '&')
|
|
318
|
+
.replace(/</g, '<')
|
|
319
|
+
.replace(/>/g, '>')
|
|
320
|
+
.replace(/"/g, '"')
|
|
321
|
+
.replace(/'/g, ''');
|
|
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
|
+
};
|