@power-seo/content-analysis 1.0.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/dist/react.cjs ADDED
@@ -0,0 +1,677 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@power-seo/core');
5
+
6
+ // src/react.ts
7
+ function checkTitle(input) {
8
+ const results = [];
9
+ const { title, focusKeyphrase } = input;
10
+ if (!title || title.trim().length === 0) {
11
+ results.push({
12
+ id: "title-presence",
13
+ title: "SEO title",
14
+ description: "No title has been set. Add a title to improve search visibility.",
15
+ status: "poor",
16
+ score: 0,
17
+ maxScore: 5
18
+ });
19
+ return results;
20
+ }
21
+ const validation = core.validateTitle(title);
22
+ if (!validation.valid) {
23
+ results.push({
24
+ id: "title-presence",
25
+ title: "SEO title",
26
+ description: validation.message,
27
+ status: "ok",
28
+ score: 3,
29
+ maxScore: 5
30
+ });
31
+ } else if (validation.severity === "warning") {
32
+ results.push({
33
+ id: "title-presence",
34
+ title: "SEO title",
35
+ description: validation.message,
36
+ status: "ok",
37
+ score: 3,
38
+ maxScore: 5
39
+ });
40
+ } else {
41
+ results.push({
42
+ id: "title-presence",
43
+ title: "SEO title",
44
+ description: validation.message,
45
+ status: "good",
46
+ score: 5,
47
+ maxScore: 5
48
+ });
49
+ }
50
+ if (focusKeyphrase && focusKeyphrase.trim().length > 0) {
51
+ const kp = focusKeyphrase.toLowerCase().trim();
52
+ const titleLower = title.toLowerCase();
53
+ if (titleLower.includes(kp)) {
54
+ results.push({
55
+ id: "title-keyphrase",
56
+ title: "Keyphrase in title",
57
+ description: "The focus keyphrase appears in the SEO title. Good job!",
58
+ status: "good",
59
+ score: 5,
60
+ maxScore: 5
61
+ });
62
+ } else {
63
+ results.push({
64
+ id: "title-keyphrase",
65
+ title: "Keyphrase in title",
66
+ description: "The focus keyphrase does not appear in the SEO title. Add it to improve relevance.",
67
+ status: "ok",
68
+ score: 2,
69
+ maxScore: 5
70
+ });
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+ function checkMetaDescription(input) {
76
+ const results = [];
77
+ const { metaDescription, focusKeyphrase } = input;
78
+ if (!metaDescription || metaDescription.trim().length === 0) {
79
+ results.push({
80
+ id: "meta-description-presence",
81
+ title: "Meta description",
82
+ description: "No meta description has been set. Add one to control how your page appears in search results.",
83
+ status: "poor",
84
+ score: 0,
85
+ maxScore: 5
86
+ });
87
+ return results;
88
+ }
89
+ const validation = core.validateMetaDescription(metaDescription);
90
+ if (!validation.valid) {
91
+ results.push({
92
+ id: "meta-description-presence",
93
+ title: "Meta description",
94
+ description: validation.message,
95
+ status: "ok",
96
+ score: 3,
97
+ maxScore: 5
98
+ });
99
+ } else if (validation.severity === "warning") {
100
+ results.push({
101
+ id: "meta-description-presence",
102
+ title: "Meta description",
103
+ description: validation.message,
104
+ status: "ok",
105
+ score: 3,
106
+ maxScore: 5
107
+ });
108
+ } else {
109
+ results.push({
110
+ id: "meta-description-presence",
111
+ title: "Meta description",
112
+ description: validation.message,
113
+ status: "good",
114
+ score: 5,
115
+ maxScore: 5
116
+ });
117
+ }
118
+ if (focusKeyphrase && focusKeyphrase.trim().length > 0) {
119
+ const kp = focusKeyphrase.toLowerCase().trim();
120
+ const descLower = metaDescription.toLowerCase();
121
+ if (descLower.includes(kp)) {
122
+ results.push({
123
+ id: "meta-description-keyphrase",
124
+ title: "Keyphrase in meta description",
125
+ description: "The focus keyphrase appears in the meta description. Well done!",
126
+ status: "good",
127
+ score: 5,
128
+ maxScore: 5
129
+ });
130
+ } else {
131
+ results.push({
132
+ id: "meta-description-keyphrase",
133
+ title: "Keyphrase in meta description",
134
+ description: "The focus keyphrase does not appear in the meta description. Add it to improve click-through rate.",
135
+ status: "ok",
136
+ score: 2,
137
+ maxScore: 5
138
+ });
139
+ }
140
+ }
141
+ return results;
142
+ }
143
+ function checkKeyphraseUsage(input) {
144
+ const results = [];
145
+ const { focusKeyphrase, title, metaDescription, content, slug, images } = input;
146
+ if (!focusKeyphrase || focusKeyphrase.trim().length === 0) {
147
+ results.push({
148
+ id: "keyphrase-density",
149
+ title: "Keyphrase density",
150
+ description: "No focus keyphrase set. Set one to get keyphrase analysis.",
151
+ status: "good",
152
+ score: 5,
153
+ maxScore: 5
154
+ });
155
+ return results;
156
+ }
157
+ const occurrences = core.analyzeKeyphraseOccurrences({
158
+ keyphrase: focusKeyphrase,
159
+ title,
160
+ metaDescription,
161
+ content,
162
+ slug,
163
+ images
164
+ });
165
+ const densityResult = core.calculateKeywordDensity(focusKeyphrase, content);
166
+ if (densityResult.density < core.KEYWORD_DENSITY.MIN) {
167
+ results.push({
168
+ id: "keyphrase-density",
169
+ title: "Keyphrase density",
170
+ description: `Keyphrase density is ${densityResult.density}%, which is below the recommended minimum of ${core.KEYWORD_DENSITY.MIN}%. Use the keyphrase more often.`,
171
+ status: "poor",
172
+ score: 1,
173
+ maxScore: 5
174
+ });
175
+ } else if (densityResult.density > core.KEYWORD_DENSITY.MAX) {
176
+ results.push({
177
+ id: "keyphrase-density",
178
+ title: "Keyphrase density",
179
+ description: `Keyphrase density is ${densityResult.density}%, which exceeds the recommended maximum of ${core.KEYWORD_DENSITY.MAX}%. Reduce usage to avoid keyword stuffing.`,
180
+ status: "poor",
181
+ score: 1,
182
+ maxScore: 5
183
+ });
184
+ } else if (densityResult.density >= core.KEYWORD_DENSITY.MIN && densityResult.density <= core.KEYWORD_DENSITY.MAX) {
185
+ const isOptimal = Math.abs(densityResult.density - core.KEYWORD_DENSITY.OPTIMAL) < 0.5;
186
+ results.push({
187
+ id: "keyphrase-density",
188
+ title: "Keyphrase density",
189
+ description: `Keyphrase density is ${densityResult.density}%.${isOptimal ? " Great \u2014 this is close to the optimal density." : " This is within the recommended range."}`,
190
+ status: "good",
191
+ score: 5,
192
+ maxScore: 5
193
+ });
194
+ }
195
+ const distributionPoints = [];
196
+ if (!occurrences.inFirstParagraph) distributionPoints.push("introduction");
197
+ if (!occurrences.inH1 && occurrences.inHeadings === 0) distributionPoints.push("headings");
198
+ if (!occurrences.inSlug) distributionPoints.push("slug");
199
+ if (occurrences.inAltText === 0 && images && images.length > 0)
200
+ distributionPoints.push("image alt text");
201
+ if (distributionPoints.length === 0) {
202
+ results.push({
203
+ id: "keyphrase-distribution",
204
+ title: "Keyphrase distribution",
205
+ description: "The focus keyphrase is well-distributed across the introduction, headings, slug, and image alt text.",
206
+ status: "good",
207
+ score: 5,
208
+ maxScore: 5
209
+ });
210
+ } else if (distributionPoints.length <= 2) {
211
+ results.push({
212
+ id: "keyphrase-distribution",
213
+ title: "Keyphrase distribution",
214
+ description: `Consider adding the keyphrase to: ${distributionPoints.join(", ")}.`,
215
+ status: "ok",
216
+ score: 3,
217
+ maxScore: 5
218
+ });
219
+ } else {
220
+ results.push({
221
+ id: "keyphrase-distribution",
222
+ title: "Keyphrase distribution",
223
+ description: `The keyphrase is missing from: ${distributionPoints.join(", ")}. Distribute it more broadly.`,
224
+ status: "poor",
225
+ score: 1,
226
+ maxScore: 5
227
+ });
228
+ }
229
+ return results;
230
+ }
231
+ function parseHeadings(html) {
232
+ const headings = [];
233
+ const regex = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
234
+ let match;
235
+ while ((match = regex.exec(html)) !== null) {
236
+ headings.push({
237
+ level: parseInt(match[1], 10),
238
+ text: core.stripHtml(match[2])
239
+ });
240
+ }
241
+ return headings;
242
+ }
243
+ function checkHeadings(input) {
244
+ const results = [];
245
+ const { content, focusKeyphrase } = input;
246
+ const headings = parseHeadings(content);
247
+ const h1s = headings.filter((h) => h.level === 1);
248
+ if (h1s.length === 0) {
249
+ results.push({
250
+ id: "heading-structure",
251
+ title: "Heading structure",
252
+ description: "No H1 heading found. Add exactly one H1 as the main heading of your page.",
253
+ status: "poor",
254
+ score: 0,
255
+ maxScore: 5
256
+ });
257
+ } else if (h1s.length > 1) {
258
+ results.push({
259
+ id: "heading-structure",
260
+ title: "Heading structure",
261
+ description: `Found ${h1s.length} H1 headings. Use exactly one H1 per page for proper SEO.`,
262
+ status: "ok",
263
+ score: 3,
264
+ maxScore: 5
265
+ });
266
+ } else {
267
+ let hasSkippedLevel = false;
268
+ for (let i = 1; i < headings.length; i++) {
269
+ const prev = headings[i - 1];
270
+ const curr = headings[i];
271
+ if (curr.level > prev.level + 1) {
272
+ hasSkippedLevel = true;
273
+ break;
274
+ }
275
+ }
276
+ if (hasSkippedLevel) {
277
+ results.push({
278
+ id: "heading-structure",
279
+ title: "Heading structure",
280
+ description: "The heading hierarchy skips levels (e.g., H2 to H4). Use sequential heading levels for better accessibility and SEO.",
281
+ status: "ok",
282
+ score: 3,
283
+ maxScore: 5
284
+ });
285
+ } else {
286
+ results.push({
287
+ id: "heading-structure",
288
+ title: "Heading structure",
289
+ description: "The heading structure looks good with a single H1 and proper hierarchy.",
290
+ status: "good",
291
+ score: 5,
292
+ maxScore: 5
293
+ });
294
+ }
295
+ }
296
+ if (focusKeyphrase && focusKeyphrase.trim().length > 0) {
297
+ const kp = focusKeyphrase.toLowerCase().trim();
298
+ const subheadings = headings.filter((h) => h.level >= 2);
299
+ const hasKeyphraseInSubheading = subheadings.some((h) => h.text.toLowerCase().includes(kp));
300
+ if (subheadings.length === 0) {
301
+ results.push({
302
+ id: "heading-keyphrase",
303
+ title: "Keyphrase in subheadings",
304
+ description: "No subheadings (H2-H6) found. Add subheadings to structure your content and include the focus keyphrase.",
305
+ status: "ok",
306
+ score: 2,
307
+ maxScore: 5
308
+ });
309
+ } else if (hasKeyphraseInSubheading) {
310
+ results.push({
311
+ id: "heading-keyphrase",
312
+ title: "Keyphrase in subheadings",
313
+ description: "The focus keyphrase appears in at least one subheading. Nice!",
314
+ status: "good",
315
+ score: 5,
316
+ maxScore: 5
317
+ });
318
+ } else {
319
+ results.push({
320
+ id: "heading-keyphrase",
321
+ title: "Keyphrase in subheadings",
322
+ description: "The focus keyphrase does not appear in any subheading. Consider adding it to an H2 or H3.",
323
+ status: "ok",
324
+ score: 2,
325
+ maxScore: 5
326
+ });
327
+ }
328
+ }
329
+ return results;
330
+ }
331
+ function checkWordCount(input) {
332
+ const words = core.getWords(input.content);
333
+ const count = words.length;
334
+ if (count < core.MIN_WORD_COUNT) {
335
+ return {
336
+ id: "word-count",
337
+ title: "Word count",
338
+ description: `The content is ${count} words, which is below the recommended minimum of ${core.MIN_WORD_COUNT}. Add more content to improve SEO.`,
339
+ status: "poor",
340
+ score: 1,
341
+ maxScore: 5
342
+ };
343
+ }
344
+ if (count < core.RECOMMENDED_WORD_COUNT) {
345
+ return {
346
+ id: "word-count",
347
+ title: "Word count",
348
+ description: `The content is ${count} words. Consider expanding to at least ${core.RECOMMENDED_WORD_COUNT} words for more comprehensive coverage.`,
349
+ status: "ok",
350
+ score: 3,
351
+ maxScore: 5
352
+ };
353
+ }
354
+ return {
355
+ id: "word-count",
356
+ title: "Word count",
357
+ description: `The content is ${count} words. Good \u2014 this provides enough depth for search engines.`,
358
+ status: "good",
359
+ score: 5,
360
+ maxScore: 5
361
+ };
362
+ }
363
+
364
+ // src/checks/images.ts
365
+ function checkImages(input) {
366
+ const results = [];
367
+ const { images, focusKeyphrase } = input;
368
+ if (!images || images.length === 0) {
369
+ results.push({
370
+ id: "image-alt",
371
+ title: "Image alt attributes",
372
+ description: "No images found. Consider adding images to make your content more engaging.",
373
+ status: "ok",
374
+ score: 3,
375
+ maxScore: 5
376
+ });
377
+ return results;
378
+ }
379
+ const missingAlt = images.filter((img) => !img.alt || img.alt.trim().length === 0);
380
+ if (missingAlt.length === 0) {
381
+ results.push({
382
+ id: "image-alt",
383
+ title: "Image alt attributes",
384
+ description: "All images have alt text. Great for accessibility and SEO!",
385
+ status: "good",
386
+ score: 5,
387
+ maxScore: 5
388
+ });
389
+ } else if (missingAlt.length === images.length) {
390
+ results.push({
391
+ id: "image-alt",
392
+ title: "Image alt attributes",
393
+ description: "None of the images have alt text. Add descriptive alt attributes for accessibility and SEO.",
394
+ status: "poor",
395
+ score: 0,
396
+ maxScore: 5
397
+ });
398
+ } else {
399
+ results.push({
400
+ id: "image-alt",
401
+ title: "Image alt attributes",
402
+ description: `${missingAlt.length} of ${images.length} images are missing alt text. Add alt attributes to all images.`,
403
+ status: "ok",
404
+ score: 2,
405
+ maxScore: 5
406
+ });
407
+ }
408
+ if (focusKeyphrase && focusKeyphrase.trim().length > 0) {
409
+ const kp = focusKeyphrase.toLowerCase().trim();
410
+ const hasKeyphraseInAlt = images.some(
411
+ (img) => img.alt && img.alt.toLowerCase().includes(kp)
412
+ );
413
+ if (hasKeyphraseInAlt) {
414
+ results.push({
415
+ id: "image-keyphrase",
416
+ title: "Keyphrase in image alt",
417
+ description: "The focus keyphrase appears in at least one image alt attribute.",
418
+ status: "good",
419
+ score: 5,
420
+ maxScore: 5
421
+ });
422
+ } else {
423
+ results.push({
424
+ id: "image-keyphrase",
425
+ title: "Keyphrase in image alt",
426
+ description: "The focus keyphrase does not appear in any image alt attribute. Add it to at least one image.",
427
+ status: "ok",
428
+ score: 2,
429
+ maxScore: 5
430
+ });
431
+ }
432
+ }
433
+ return results;
434
+ }
435
+
436
+ // src/checks/links.ts
437
+ function checkLinks(input) {
438
+ const results = [];
439
+ const { internalLinks, externalLinks } = input;
440
+ const hasInternal = internalLinks && internalLinks.length > 0;
441
+ const hasExternal = externalLinks && externalLinks.length > 0;
442
+ if (!hasInternal) {
443
+ results.push({
444
+ id: "internal-links",
445
+ title: "Internal links",
446
+ description: "No internal links found. Add links to other pages on your site to improve crawlability and distribute link equity.",
447
+ status: "ok",
448
+ score: 2,
449
+ maxScore: 5
450
+ });
451
+ } else {
452
+ results.push({
453
+ id: "internal-links",
454
+ title: "Internal links",
455
+ description: `Found ${internalLinks.length} internal link${internalLinks.length === 1 ? "" : "s"}. Good for site structure and SEO.`,
456
+ status: "good",
457
+ score: 5,
458
+ maxScore: 5
459
+ });
460
+ }
461
+ if (!hasExternal) {
462
+ results.push({
463
+ id: "external-links",
464
+ title: "External links",
465
+ description: "No external links found. Consider adding outbound links to authoritative sources to strengthen your content.",
466
+ status: "ok",
467
+ score: 2,
468
+ maxScore: 5
469
+ });
470
+ } else {
471
+ results.push({
472
+ id: "external-links",
473
+ title: "External links",
474
+ description: `Found ${externalLinks.length} external link${externalLinks.length === 1 ? "" : "s"}. Linking to quality sources adds credibility.`,
475
+ status: "good",
476
+ score: 5,
477
+ maxScore: 5
478
+ });
479
+ }
480
+ return results;
481
+ }
482
+
483
+ // src/analyzer.ts
484
+ function analyzeContent(input, config) {
485
+ const disabled = new Set(config?.disabledChecks ?? []);
486
+ const allResults = [];
487
+ const titleResults = checkTitle(input);
488
+ const metaResults = checkMetaDescription(input);
489
+ const keyphraseResults = checkKeyphraseUsage(input);
490
+ const headingResults = checkHeadings(input);
491
+ const wordCountResult = checkWordCount(input);
492
+ const imageResults = checkImages(input);
493
+ const linkResults = checkLinks(input);
494
+ const candidateResults = [
495
+ ...titleResults,
496
+ ...metaResults,
497
+ ...keyphraseResults,
498
+ ...headingResults,
499
+ wordCountResult,
500
+ ...imageResults,
501
+ ...linkResults
502
+ ];
503
+ for (const result of candidateResults) {
504
+ if (!disabled.has(result.id)) {
505
+ allResults.push(result);
506
+ }
507
+ }
508
+ const score = allResults.reduce((sum, r) => sum + r.score, 0);
509
+ const maxScore = allResults.reduce((sum, r) => sum + r.maxScore, 0);
510
+ const recommendations = allResults.filter((r) => r.status === "poor" || r.status === "ok").map((r) => r.description);
511
+ return {
512
+ score,
513
+ maxScore,
514
+ results: allResults,
515
+ recommendations
516
+ };
517
+ }
518
+
519
+ // src/react.ts
520
+ function getScoreColor(percentage) {
521
+ if (percentage >= 70) return "#1e8e3e";
522
+ if (percentage >= 40) return "#f29900";
523
+ return "#d93025";
524
+ }
525
+ function getScoreLabel(percentage) {
526
+ if (percentage >= 70) return "Good";
527
+ if (percentage >= 40) return "OK";
528
+ return "Needs improvement";
529
+ }
530
+ function ScorePanel({ score, maxScore }) {
531
+ const percentage = maxScore > 0 ? Math.round(score / maxScore * 100) : 0;
532
+ const color = getScoreColor(percentage);
533
+ const label = getScoreLabel(percentage);
534
+ return react.createElement(
535
+ "div",
536
+ {
537
+ style: {
538
+ fontFamily: "system-ui, -apple-system, sans-serif",
539
+ padding: "16px",
540
+ borderRadius: "8px",
541
+ border: "1px solid #e0e0e0",
542
+ backgroundColor: "#fff"
543
+ }
544
+ },
545
+ react.createElement(
546
+ "div",
547
+ {
548
+ style: {
549
+ display: "flex",
550
+ justifyContent: "space-between",
551
+ alignItems: "center",
552
+ marginBottom: "8px"
553
+ }
554
+ },
555
+ react.createElement(
556
+ "span",
557
+ { style: { fontWeight: 600, fontSize: "14px", color: "#333" } },
558
+ "SEO Score"
559
+ ),
560
+ react.createElement(
561
+ "span",
562
+ { style: { fontWeight: 700, fontSize: "18px", color } },
563
+ `${percentage}%`
564
+ )
565
+ ),
566
+ react.createElement(
567
+ "div",
568
+ {
569
+ style: {
570
+ width: "100%",
571
+ height: "8px",
572
+ backgroundColor: "#e8e8e8",
573
+ borderRadius: "4px",
574
+ overflow: "hidden"
575
+ }
576
+ },
577
+ react.createElement("div", {
578
+ style: {
579
+ width: `${percentage}%`,
580
+ height: "100%",
581
+ backgroundColor: color,
582
+ borderRadius: "4px",
583
+ transition: "width 0.3s ease"
584
+ }
585
+ })
586
+ ),
587
+ react.createElement(
588
+ "div",
589
+ { style: { marginTop: "4px", fontSize: "12px", color: "#666" } },
590
+ `${label} \u2014 ${score}/${maxScore} points`
591
+ )
592
+ );
593
+ }
594
+ var STATUS_ICONS = {
595
+ good: "\u2705",
596
+ ok: "\u26A0\uFE0F",
597
+ poor: "\u274C"
598
+ };
599
+ var STATUS_COLORS = {
600
+ good: "#1e8e3e",
601
+ ok: "#f29900",
602
+ poor: "#d93025"
603
+ };
604
+ function CheckList({ results }) {
605
+ return react.createElement(
606
+ "ul",
607
+ {
608
+ style: {
609
+ listStyle: "none",
610
+ padding: 0,
611
+ margin: 0,
612
+ fontFamily: "system-ui, -apple-system, sans-serif"
613
+ }
614
+ },
615
+ ...results.map(
616
+ (result) => react.createElement(
617
+ "li",
618
+ {
619
+ key: result.id,
620
+ style: {
621
+ padding: "10px 12px",
622
+ borderBottom: "1px solid #f0f0f0",
623
+ display: "flex",
624
+ gap: "10px",
625
+ alignItems: "flex-start"
626
+ }
627
+ },
628
+ react.createElement(
629
+ "span",
630
+ { style: { flexShrink: 0, fontSize: "14px" } },
631
+ STATUS_ICONS[result.status] ?? ""
632
+ ),
633
+ react.createElement(
634
+ "div",
635
+ { style: { flex: 1 } },
636
+ react.createElement(
637
+ "div",
638
+ {
639
+ style: {
640
+ fontWeight: 600,
641
+ fontSize: "13px",
642
+ color: STATUS_COLORS[result.status] ?? "#333"
643
+ }
644
+ },
645
+ result.title
646
+ ),
647
+ react.createElement(
648
+ "div",
649
+ { style: { fontSize: "12px", color: "#555", marginTop: "2px" } },
650
+ result.description
651
+ )
652
+ )
653
+ )
654
+ )
655
+ );
656
+ }
657
+ function ContentAnalyzer({ input, config, children }) {
658
+ const output = react.useMemo(() => analyzeContent(input, config), [input, config]);
659
+ return react.createElement(
660
+ "div",
661
+ {
662
+ style: {
663
+ fontFamily: "system-ui, -apple-system, sans-serif"
664
+ }
665
+ },
666
+ react.createElement(ScorePanel, { score: output.score, maxScore: output.maxScore }),
667
+ react.createElement("div", { style: { height: "12px" } }),
668
+ react.createElement(CheckList, { results: output.results }),
669
+ children ?? null
670
+ );
671
+ }
672
+
673
+ exports.CheckList = CheckList;
674
+ exports.ContentAnalyzer = ContentAnalyzer;
675
+ exports.ScorePanel = ScorePanel;
676
+ //# sourceMappingURL=react.cjs.map
677
+ //# sourceMappingURL=react.cjs.map