@knowcode/doc-builder 1.5.6 → 1.5.8

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/cli.js +132 -45
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to @knowcode/doc-builder will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.8] - 2025-07-22
9
+
10
+ ### Changed
11
+ - **BREAKING**: `seo-check` command now analyzes generated HTML files instead of markdown source files
12
+ - Completely rewrote SEO analysis to inspect actual HTML output that search engines see
13
+ - Added comprehensive checks for all SEO elements in HTML including:
14
+ - Title tags and optimal length
15
+ - Meta descriptions and character limits
16
+ - Keywords meta tags
17
+ - Canonical URLs
18
+ - H1 tags and consistency with titles
19
+ - Open Graph tags for social media
20
+ - Twitter Card tags
21
+ - Structured data (JSON-LD)
22
+ - Updated help text and examples to reflect HTML analysis
23
+ - Improved error messages to guide users to build HTML first
24
+
25
+ ### Why This Change?
26
+ - SEO analysis should check what search engines actually see (the generated HTML)
27
+ - Provides more accurate and comprehensive SEO insights
28
+ - Can verify that all meta tags are properly generated
29
+ - Ensures Open Graph and Twitter Cards are working correctly
30
+
31
+ ## [1.5.7] - 2025-07-22
32
+
33
+ ### Fixed
34
+ - Fixed "path argument must be string" error when running seo-check via npx
35
+ - Properly pass options to loadConfig in seo-check command
36
+ - Added validation to check if docs directory exists before running SEO analysis
37
+ - Improved error messages when docs directory is missing
38
+
8
39
  ## [1.5.6] - 2025-07-22
9
40
 
10
41
  ### Fixed
package/cli.js CHANGED
@@ -196,28 +196,38 @@ ${chalk.yellow('Get your verification code from Google Search Console.')}
196
196
  // SEO Check command
197
197
  program
198
198
  .command('seo-check [path]')
199
- .description('Analyze SEO for your documentation pages')
199
+ .description('Analyze SEO metadata in generated HTML files')
200
200
  .option('-c, --config <path>', 'path to config file (default: doc-builder.config.js)')
201
- .option('--fix', 'auto-fix common SEO issues')
202
201
  .addHelpText('after', `
203
202
  ${chalk.yellow('Examples:')}
204
- ${chalk.gray('$')} doc-builder seo-check ${chalk.gray('# Check all pages')}
205
- ${chalk.gray('$')} doc-builder seo-check docs/guide.md ${chalk.gray('# Check specific page')}
206
- ${chalk.gray('$')} doc-builder seo-check --fix ${chalk.gray('# Auto-fix issues')}
207
-
208
- ${chalk.yellow('This command analyzes:')}
209
- Title length and optimization (50-60 characters)
210
- Meta descriptions (140-160 characters)
211
- Keywords usage and relevance
212
- Front matter SEO fields
213
- Duplicate content issues
203
+ ${chalk.gray('$')} doc-builder seo-check ${chalk.gray('# Check all HTML pages')}
204
+ ${chalk.gray('$')} doc-builder seo-check html/guide.html ${chalk.gray('# Check specific HTML file')}
205
+
206
+ ${chalk.yellow('This command analyzes generated HTML files for:')}
207
+ • Title tags and length (50-60 characters)
208
+ Meta descriptions (140-160 characters)
209
+ Keywords meta tags
210
+ Canonical URLs
211
+ H1 tags and consistency
212
+ Open Graph tags (Facebook)
213
+ • Twitter Card tags
214
+ • Structured data (JSON-LD)
215
+
216
+ ${chalk.yellow('Note:')} Run 'doc-builder build' first to generate HTML files.
214
217
  `)
215
218
  .action(async (filePath, options) => {
216
219
  try {
217
- const config = await loadConfig(options.config);
218
- const docsDir = path.resolve(config.docsDir || 'docs');
220
+ const config = await loadConfig(options.config || 'doc-builder.config.js', options);
221
+ const outputDir = path.resolve(config.outputDir || 'html');
222
+
223
+ // Check if outputDir exists
224
+ if (!await fs.pathExists(outputDir)) {
225
+ console.error(chalk.red(`Output directory not found: ${outputDir}`));
226
+ console.log(chalk.yellow('\nPlease build your documentation first with: doc-builder build'));
227
+ process.exit(1);
228
+ }
219
229
 
220
- console.log(chalk.cyan('🔍 Analyzing SEO...'));
230
+ console.log(chalk.cyan('🔍 Analyzing SEO in generated HTML files...'));
221
231
 
222
232
  // Get files to check
223
233
  let files = [];
@@ -230,7 +240,7 @@ ${chalk.yellow('This command analyzes:')}
230
240
  process.exit(1);
231
241
  }
232
242
  } else {
233
- // Get all markdown files
243
+ // Get all HTML files
234
244
  const getAllFiles = async (dir) => {
235
245
  const results = [];
236
246
  const items = await fs.readdir(dir);
@@ -239,13 +249,13 @@ ${chalk.yellow('This command analyzes:')}
239
249
  const stat = await fs.stat(fullPath);
240
250
  if (stat.isDirectory() && !item.startsWith('.')) {
241
251
  results.push(...await getAllFiles(fullPath));
242
- } else if (item.endsWith('.md')) {
252
+ } else if (item.endsWith('.html') && item !== '404.html') {
243
253
  results.push(fullPath);
244
254
  }
245
255
  }
246
256
  return results;
247
257
  };
248
- files = await getAllFiles(docsDir);
258
+ files = await getAllFiles(outputDir);
249
259
  }
250
260
 
251
261
  // Analyze each file
@@ -254,19 +264,43 @@ ${chalk.yellow('This command analyzes:')}
254
264
 
255
265
  for (const file of files) {
256
266
  const content = await fs.readFile(file, 'utf-8');
257
- const { data: frontMatter, content: mainContent } = matter(content);
258
- const relativePath = path.relative(docsDir, file);
267
+ const relativePath = path.relative(outputDir, file);
268
+
269
+ // Extract metadata from HTML
270
+ const titleMatch = content.match(/<title>([^<]+)<\/title>/);
271
+ const descMatch = content.match(/<meta\s+name="description"\s+content="([^"]+)"/);
272
+ const keywordsMatch = content.match(/<meta\s+name="keywords"\s+content="([^"]+)"/);
273
+ const canonicalMatch = content.match(/<link\s+rel="canonical"\s+href="([^"]+)"/);
274
+ const h1Match = content.match(/<h1>([^<]+)<\/h1>/);
259
275
 
260
- // Extract current metadata
261
- const h1Match = mainContent.match(/^#\s+(.+)$/m);
262
- const h1Title = h1Match ? h1Match[1] : null;
263
- const title = frontMatter.title || h1Title || path.basename(file, '.md');
264
- const description = frontMatter.description || generateDescription(mainContent, 'smart');
265
- const keywords = frontMatter.keywords || [];
276
+ const title = titleMatch ? titleMatch[1] : 'No title found';
277
+ const description = descMatch ? descMatch[1] : '';
278
+ const keywords = keywordsMatch ? keywordsMatch[1].split(',').map(k => k.trim()) : [];
279
+ const canonical = canonicalMatch ? canonicalMatch[1] : '';
280
+ const h1 = h1Match ? h1Match[1] : '';
281
+
282
+ // Check for Open Graph tags
283
+ const ogTitleMatch = content.match(/<meta\s+property="og:title"\s+content="([^"]+)"/);
284
+ const ogDescMatch = content.match(/<meta\s+property="og:description"\s+content="([^"]+)"/);
285
+ const ogImageMatch = content.match(/<meta\s+property="og:image"\s+content="([^"]+)"/);
286
+
287
+ // Check for Twitter Card tags
288
+ const twitterTitleMatch = content.match(/<meta\s+name="twitter:title"\s+content="([^"]+)"/);
289
+ const twitterDescMatch = content.match(/<meta\s+name="twitter:description"\s+content="([^"]+)"/);
290
+
291
+ // Check for structured data
292
+ const structuredDataMatch = content.match(/<script\s+type="application\/ld\+json">/);
266
293
 
267
294
  // Check title
268
295
  const titleLength = title.length;
269
- if (titleLength > 60) {
296
+ if (!titleMatch) {
297
+ issues.push({
298
+ file: relativePath,
299
+ type: 'title',
300
+ message: 'Missing <title> tag',
301
+ severity: 'critical'
302
+ });
303
+ } else if (titleLength > 60) {
270
304
  issues.push({
271
305
  file: relativePath,
272
306
  type: 'title',
@@ -283,38 +317,90 @@ ${chalk.yellow('This command analyzes:')}
283
317
  }
284
318
 
285
319
  // Check description
286
- const descLength = description.length;
287
- if (descLength > 160) {
320
+ if (!descMatch) {
288
321
  issues.push({
289
322
  file: relativePath,
290
323
  type: 'description',
291
- message: `Description too long (${descLength} chars, max 160)`,
324
+ message: 'Missing meta description',
325
+ severity: 'critical'
326
+ });
327
+ } else {
328
+ const descLength = description.length;
329
+ if (descLength > 160) {
330
+ issues.push({
331
+ file: relativePath,
332
+ type: 'description',
333
+ message: `Description too long (${descLength} chars, max 160)`,
292
334
  current: description,
293
335
  suggestion: description.substring(0, 157) + '...'
294
336
  });
295
- } else if (descLength < 120) {
337
+ } else if (descLength < 120) {
338
+ suggestions.push({
339
+ file: relativePath,
340
+ type: 'description',
341
+ message: `Description might be too short (${descLength} chars, ideal 140-160)`
342
+ });
343
+ }
344
+ }
345
+
346
+ // Check keywords
347
+ if (!keywordsMatch || keywords.length === 0) {
296
348
  suggestions.push({
297
349
  file: relativePath,
298
- type: 'description',
299
- message: `Description might be too short (${descLength} chars, ideal 140-160)`
350
+ type: 'keywords',
351
+ message: 'No keywords meta tag found'
300
352
  });
301
353
  }
302
354
 
303
- // Check keywords
304
- if (keywords.length === 0 && !frontMatter.description) {
355
+ // Check canonical URL
356
+ if (!canonicalMatch) {
305
357
  suggestions.push({
306
358
  file: relativePath,
307
- type: 'keywords',
308
- message: 'No keywords defined in front matter'
359
+ type: 'canonical',
360
+ message: 'Missing canonical URL'
361
+ });
362
+ }
363
+
364
+ // Check H1
365
+ if (!h1Match) {
366
+ issues.push({
367
+ file: relativePath,
368
+ type: 'h1',
369
+ message: 'Missing H1 tag',
370
+ severity: 'important'
371
+ });
372
+ } else if (h1 !== title.split(' | ')[0]) {
373
+ suggestions.push({
374
+ file: relativePath,
375
+ type: 'h1',
376
+ message: 'H1 differs from page title'
377
+ });
378
+ }
379
+
380
+ // Check Open Graph tags
381
+ if (!ogTitleMatch || !ogDescMatch) {
382
+ suggestions.push({
383
+ file: relativePath,
384
+ type: 'opengraph',
385
+ message: 'Missing or incomplete Open Graph tags'
386
+ });
387
+ }
388
+
389
+ // Check Twitter Card tags
390
+ if (!twitterTitleMatch || !twitterDescMatch) {
391
+ suggestions.push({
392
+ file: relativePath,
393
+ type: 'twitter',
394
+ message: 'Missing or incomplete Twitter Card tags'
309
395
  });
310
396
  }
311
397
 
312
- // Check for front matter
313
- if (!frontMatter.title && !frontMatter.description) {
398
+ // Check structured data
399
+ if (!structuredDataMatch) {
314
400
  suggestions.push({
315
401
  file: relativePath,
316
- type: 'frontmatter',
317
- message: 'No SEO front matter found'
402
+ type: 'structured-data',
403
+ message: 'Missing structured data (JSON-LD)'
318
404
  });
319
405
  }
320
406
  }
@@ -345,15 +431,16 @@ ${chalk.yellow('This command analyzes:')}
345
431
  });
346
432
  }
347
433
 
348
- console.log(`\n${chalk.cyan('💡 Tips:')}`);
349
- console.log(' • Add front matter to customize SEO per page');
434
+ console.log(`\n${chalk.cyan('💡 Tips to improve SEO:')}`);
435
+ console.log(' • Add front matter to markdown files to customize SEO');
350
436
  console.log(' • Keep titles between 50-60 characters');
351
437
  console.log(' • Write descriptions between 140-160 characters');
352
438
  console.log(' • Include relevant keywords in front matter');
353
- console.log(`\n${chalk.gray('Example front matter:')}`);
439
+ console.log(' Ensure each page has a unique title and description');
440
+ console.log(`\n${chalk.gray('Add to your markdown files:')}`);
354
441
  console.log(chalk.gray('---'));
355
442
  console.log(chalk.gray('title: "Your SEO Optimized Title Here"'));
356
- console.log(chalk.gray('description: "A compelling description between 140-160 characters that includes keywords and encourages clicks."'));
443
+ console.log(chalk.gray('description: "A compelling description between 140-160 characters."'));
357
444
  console.log(chalk.gray('keywords: ["keyword1", "keyword2", "keyword3"]'));
358
445
  console.log(chalk.gray('---'));
359
446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowcode/doc-builder",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "description": "Reusable documentation builder for markdown-based sites with Vercel deployment support",
5
5
  "main": "index.js",
6
6
  "bin": {