@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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +52 -0
- package/README.md +57 -7
- package/assets/404.html +115 -0
- package/assets/js/main.js +23 -0
- package/cli.js +251 -2
- package/doc-builder.config.js +34 -0
- package/html/404.html +115 -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/js/main.js +23 -0
- package/html/robots.txt +5 -0
- package/html/sitemap.xml +75 -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 +110 -5
- package/lib/deploy.js +61 -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',
|
|
@@ -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.
|
|
116
|
-
console.log(chalk.white('
|
|
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, '&')
|
|
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
|
+
};
|