@power-seo/content-analysis 1.0.6 → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +55 -50
- package/dist/index.cjs +94 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +51 -10
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +104 -47
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +49 -11
- package/dist/react.js.map +1 -1
- package/package.json +15 -15
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CCBD SEO Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ Keyword-focused content analysis with real-time scoring, readability checks, and
|
|
|
12
12
|
[](https://www.typescriptlang.org/)
|
|
13
13
|
[](https://bundlephobia.com/package/@power-seo/content-analysis)
|
|
14
14
|
|
|
15
|
-
`@power-seo/content-analysis` delivers a comprehensive, WordPress SEO plugin–style scoring pipeline for evaluating text content, comparable to Yoast SEO, All in One SEO (AIOSEO), Rank Math, SEOPress, and The SEO Framework. Provide a page title, meta description, body content, focus keyphrase, images, and links — get back structured `good` / `
|
|
15
|
+
`@power-seo/content-analysis` delivers a comprehensive, WordPress SEO plugin–style scoring pipeline for evaluating text content, comparable to Yoast SEO, All in One SEO (AIOSEO), Rank Math, SEOPress, and The SEO Framework. Provide a page title, meta description, body content, focus keyphrase, images, and links — get back structured `good` / `ok` / `poor` results across all critical SEO factors. Run it server-side in a CMS, client-side in a React editor, or inside a CI content quality gate. All 13 analysis checks are fully configurable and tree-shakeable.
|
|
16
16
|
|
|
17
|
-
> **Zero runtime dependencies** —
|
|
17
|
+
> **Zero external runtime dependencies** — `@power-seo/core` is a direct dependency bundled with this package; no extra install needed.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -82,15 +82,15 @@ Keyword-focused content analysis with real-time scoring, readability checks, and
|
|
|
82
82
|
## Installation
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
npm install @power-seo/content-analysis
|
|
85
|
+
npm install @power-seo/content-analysis
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
yarn add @power-seo/content-analysis
|
|
89
|
+
yarn add @power-seo/content-analysis
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
pnpm add @power-seo/content-analysis
|
|
93
|
+
pnpm add @power-seo/content-analysis
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
---
|
|
@@ -103,21 +103,21 @@ import { analyzeContent } from '@power-seo/content-analysis';
|
|
|
103
103
|
const result = analyzeContent({
|
|
104
104
|
title: 'Best Running Shoes for Beginners',
|
|
105
105
|
metaDescription: 'Discover the best running shoes for beginners with our expert guide.',
|
|
106
|
-
|
|
106
|
+
focusKeyphrase: 'running shoes for beginners',
|
|
107
107
|
content: '<h1>Best Running Shoes</h1><p>Finding the right running shoes...</p>',
|
|
108
|
-
url: 'https://example.com/best-running-shoes',
|
|
109
108
|
});
|
|
110
109
|
|
|
111
|
-
console.log(result.
|
|
110
|
+
console.log(result.score); // e.g. 38
|
|
111
|
+
console.log(result.maxScore); // e.g. 55
|
|
112
112
|
console.log(result.results);
|
|
113
|
-
// [{ id: 'title-presence', status: 'good',
|
|
113
|
+
// [{ id: 'title-presence', status: 'good', description: '...', score: 5, maxScore: 5 }, ...]
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|

|
|
117
117
|
|
|
118
118
|
**Status thresholds (per check):**
|
|
119
119
|
- `good` — check fully passes
|
|
120
|
-
- `
|
|
120
|
+
- `ok` — check partially passes
|
|
121
121
|
- `poor` — check fails
|
|
122
122
|
|
|
123
123
|
---
|
|
@@ -134,16 +134,16 @@ import { analyzeContent } from '@power-seo/content-analysis';
|
|
|
134
134
|
const output = analyzeContent({
|
|
135
135
|
title: 'Next.js SEO Best Practices',
|
|
136
136
|
metaDescription: 'Learn how to optimize your Next.js app for search engines with meta tags and structured data.',
|
|
137
|
-
|
|
137
|
+
focusKeyphrase: 'next.js seo',
|
|
138
138
|
content: htmlString,
|
|
139
|
-
url: 'https://example.com/nextjs-seo',
|
|
140
139
|
images: imageList,
|
|
141
140
|
internalLinks: internalLinks,
|
|
142
141
|
externalLinks: externalLinks,
|
|
143
142
|
});
|
|
144
143
|
|
|
145
|
-
// output.
|
|
146
|
-
// output.
|
|
144
|
+
// output.score → number (sum of all check scores)
|
|
145
|
+
// output.maxScore → number (maximum possible score)
|
|
146
|
+
// output.results → AnalysisResult[]
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
### Running Individual Checks
|
|
@@ -162,20 +162,22 @@ import {
|
|
|
162
162
|
} from '@power-seo/content-analysis';
|
|
163
163
|
|
|
164
164
|
const titleResults = checkTitle({
|
|
165
|
-
title: 'React SEO Guide',
|
|
166
|
-
keyphrase: 'react seo',
|
|
167
165
|
content: '',
|
|
166
|
+
title: 'React SEO Guide',
|
|
167
|
+
focusKeyphrase: 'react seo',
|
|
168
168
|
});
|
|
169
|
-
// [
|
|
170
|
-
//
|
|
169
|
+
// [
|
|
170
|
+
// { id: 'title-presence', title: 'SEO title', description: '...', status: 'good', score: 5, maxScore: 5 },
|
|
171
|
+
// { id: 'title-keyphrase', title: 'Keyphrase in title', description: '...', status: 'good', score: 5, maxScore: 5 }
|
|
172
|
+
// ]
|
|
171
173
|
|
|
172
174
|
const wcResult = checkWordCount({ content: shortHtml });
|
|
173
|
-
// { id: 'word-count',
|
|
175
|
+
// { id: 'word-count', title: 'Word count', description: 'The content is 180 words...', status: 'poor', score: 1, maxScore: 5 }
|
|
174
176
|
```
|
|
175
177
|
|
|
176
178
|
### Disabling Specific Checks
|
|
177
179
|
|
|
178
|
-
Pass `config.disabledChecks` to
|
|
180
|
+
Pass `config.disabledChecks` to exclude specific checks from the output. Checks are still executed internally but their results are filtered from `output.results` and excluded from `score`/`maxScore` totals. Invalid check IDs are silently ignored.
|
|
179
181
|
|
|
180
182
|
```ts
|
|
181
183
|
import { analyzeContent } from '@power-seo/content-analysis';
|
|
@@ -191,6 +193,7 @@ Import from the `/react` entry point for pre-built analysis UI components:
|
|
|
191
193
|
|
|
192
194
|
```tsx
|
|
193
195
|
import { ContentAnalyzer, ScorePanel, CheckList } from '@power-seo/content-analysis/react';
|
|
196
|
+
import { analyzeContent } from '@power-seo/content-analysis';
|
|
194
197
|
import type { ContentAnalysisInput } from '@power-seo/content-analysis';
|
|
195
198
|
|
|
196
199
|
// All-in-one component
|
|
@@ -217,13 +220,13 @@ Block deploys when SEO checks fail:
|
|
|
217
220
|
```ts
|
|
218
221
|
import { analyzeContent } from '@power-seo/content-analysis';
|
|
219
222
|
|
|
220
|
-
const output = analyzeContent({ title, metaDescription,
|
|
223
|
+
const output = analyzeContent({ title, metaDescription, focusKeyphrase, content });
|
|
221
224
|
|
|
222
225
|
const failures = output.results.filter((r) => r.status === 'poor');
|
|
223
226
|
|
|
224
227
|
if (failures.length > 0) {
|
|
225
228
|
console.error('SEO checks failed:');
|
|
226
|
-
failures.forEach((r) => console.error(' ✗', r.
|
|
229
|
+
failures.forEach((r) => console.error(' ✗', r.description));
|
|
227
230
|
process.exit(1);
|
|
228
231
|
}
|
|
229
232
|
```
|
|
@@ -255,31 +258,31 @@ function analyzeContent(
|
|
|
255
258
|
| `content` | `string` | ✅ | Body HTML string |
|
|
256
259
|
| `title` | `string` | — | Page `<title>` content |
|
|
257
260
|
| `metaDescription` | `string` | — | Meta description content |
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
261
|
+
| `focusKeyphrase` | `string` | — | Focus keyphrase to analyze against |
|
|
262
|
+
| `slug` | `string` | — | URL slug (used for keyphrase-in-slug check) |
|
|
260
263
|
| `images` | `Array<{ src: string; alt?: string }>` | — | Images found on the page |
|
|
261
264
|
| `internalLinks` | `string[]` | — | Internal link URLs |
|
|
262
265
|
| `externalLinks` | `string[]` | — | External link URLs |
|
|
263
266
|
|
|
264
267
|
#### `ContentAnalysisOutput`
|
|
265
268
|
|
|
266
|
-
| Field
|
|
267
|
-
|
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
272
|
-
| `recommendations` | `string[]` | Descriptions from all failed or partial checks |
|
|
269
|
+
| Field | Type | Description |
|
|
270
|
+
| ----------------- | ------------------ | ----------------------------------------------------- |
|
|
271
|
+
| `score` | `number` | Sum of all individual check scores |
|
|
272
|
+
| `maxScore` | `number` | Maximum possible score (varies by enabled checks) |
|
|
273
|
+
| `results` | `AnalysisResult[]` | Per-check results |
|
|
274
|
+
| `recommendations` | `string[]` | Descriptions from all failed or partial checks |
|
|
273
275
|
|
|
274
276
|
#### `AnalysisResult`
|
|
275
277
|
|
|
276
|
-
| Field
|
|
277
|
-
|
|
|
278
|
-
| `id`
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
278
|
+
| Field | Type | Description |
|
|
279
|
+
| ------------- | ---------------- | --------------------------------------------------------- |
|
|
280
|
+
| `id` | `string` | Unique check identifier (one of the `CheckId` values) |
|
|
281
|
+
| `title` | `string` | Short display label for the check (e.g. `"SEO title"`) |
|
|
282
|
+
| `description` | `string` | Human-readable actionable feedback |
|
|
283
|
+
| `status` | `AnalysisStatus` | `'good'` \| `'ok'` \| `'poor'` |
|
|
284
|
+
| `score` | `number` | Points earned for this check |
|
|
285
|
+
| `maxScore` | `number` | Maximum points for this check |
|
|
283
286
|
|
|
284
287
|
#### `AnalysisConfig`
|
|
285
288
|
|
|
@@ -289,15 +292,17 @@ function analyzeContent(
|
|
|
289
292
|
|
|
290
293
|
### Individual Check Functions
|
|
291
294
|
|
|
292
|
-
| Function | Check ID(s) | Checks For
|
|
293
|
-
| ----------------------------- | -------------------------------------------------------------- |
|
|
294
|
-
| `checkTitle(input)` | `title-presence`, `title-keyphrase` | Title presence
|
|
295
|
-
| `checkMetaDescription(input)` | `meta-description-presence`, `meta-description-keyphrase` | Description presence, length (120–160 chars), keyphrase
|
|
296
|
-
| `checkKeyphraseUsage(input)` | `keyphrase-density`, `keyphrase-distribution` | Density (0.5–2.5%) and occurrence in key areas
|
|
297
|
-
| `checkHeadings(input)` | `heading-structure`, `heading-keyphrase` | H1 presence, hierarchy, keyphrase in subheadings
|
|
298
|
-
| `checkWordCount(input)` | `word-count` | Min 300 words (good at 1,000+)
|
|
299
|
-
| `checkImages(input)` | `image-alt`, `image-keyphrase` | Alt text presence and keyphrase in alt
|
|
300
|
-
| `checkLinks(input)` | `internal-links`, `external-links` | Internal and external link presence
|
|
295
|
+
| Function | Check ID(s) | Checks For |
|
|
296
|
+
| ----------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
297
|
+
| `checkTitle(input)` | `title-presence`, `title-keyphrase` | Title presence **and** length (50–60 chars, validated inside `title-presence`), keyphrase |
|
|
298
|
+
| `checkMetaDescription(input)` | `meta-description-presence`, `meta-description-keyphrase` | Description presence, length (120–160 chars), keyphrase |
|
|
299
|
+
| `checkKeyphraseUsage(input)` | `keyphrase-density`, `keyphrase-distribution` | Density (0.5–2.5%) and occurrence in key areas |
|
|
300
|
+
| `checkHeadings(input)` | `heading-structure`, `heading-keyphrase` | H1 presence, hierarchy, keyphrase in subheadings |
|
|
301
|
+
| `checkWordCount(input)` | `word-count` | Min 300 words (good at 1,000+) |
|
|
302
|
+
| `checkImages(input)` | `image-alt`, `image-keyphrase` | Alt text presence and keyphrase in alt |
|
|
303
|
+
| `checkLinks(input)` | `internal-links`, `external-links` | Internal and external link presence |
|
|
304
|
+
|
|
305
|
+
> **Note:** There is no separate `title-length` check ID. Title length validation (50–60 chars) is evaluated inside the `title-presence` check — a title that exists but is outside the recommended range returns `status: 'ok'` rather than `'good'`.
|
|
301
306
|
|
|
302
307
|
### Types
|
|
303
308
|
|
|
@@ -305,10 +310,10 @@ function analyzeContent(
|
|
|
305
310
|
| ----------------------- | --------------------------------------------------------- |
|
|
306
311
|
| `CheckId` | Union of all 13 built-in check IDs |
|
|
307
312
|
| `AnalysisConfig` | `{ disabledChecks?: CheckId[] }` |
|
|
308
|
-
| `AnalysisStatus` | `'good' \| '
|
|
313
|
+
| `AnalysisStatus` | `'good' \| 'ok' \| 'poor'` |
|
|
309
314
|
| `ContentAnalysisInput` | Input shape for `analyzeContent()` |
|
|
310
315
|
| `ContentAnalysisOutput` | Output shape from `analyzeContent()` |
|
|
311
|
-
| `AnalysisResult` | Single check result with id, status,
|
|
316
|
+
| `AnalysisResult` | Single check result with id, title, description, status, score, maxScore |
|
|
312
317
|
|
|
313
318
|
---
|
|
314
319
|
|
|
@@ -326,7 +331,7 @@ function analyzeContent(
|
|
|
326
331
|
## Architecture Overview
|
|
327
332
|
|
|
328
333
|
- **Pure TypeScript** — no compiled binary, no native modules
|
|
329
|
-
- **Zero runtime dependencies** —
|
|
334
|
+
- **Zero external runtime dependencies** — `@power-seo/core` ships as a direct dependency, no separate install
|
|
330
335
|
- **Framework-agnostic** — works in any JavaScript environment
|
|
331
336
|
- **SSR compatible** — safe to run in Next.js Server Components, Remix loaders, or Express handlers
|
|
332
337
|
- **Edge runtime safe** — no Node.js-specific APIs; runs in Cloudflare Workers, Vercel Edge, Deno
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
2
19
|
|
|
3
|
-
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
analyzeContent: () => analyzeContent,
|
|
24
|
+
checkHeadings: () => checkHeadings,
|
|
25
|
+
checkImages: () => checkImages,
|
|
26
|
+
checkKeyphraseUsage: () => checkKeyphraseUsage,
|
|
27
|
+
checkLinks: () => checkLinks,
|
|
28
|
+
checkMetaDescription: () => checkMetaDescription,
|
|
29
|
+
checkTitle: () => checkTitle,
|
|
30
|
+
checkWordCount: () => checkWordCount
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(src_exports);
|
|
4
33
|
|
|
5
34
|
// src/checks/title.ts
|
|
35
|
+
var import_core = require("@power-seo/core");
|
|
6
36
|
function checkTitle(input) {
|
|
7
37
|
const results = [];
|
|
8
38
|
const { title, focusKeyphrase } = input;
|
|
@@ -17,7 +47,7 @@ function checkTitle(input) {
|
|
|
17
47
|
});
|
|
18
48
|
return results;
|
|
19
49
|
}
|
|
20
|
-
const validation =
|
|
50
|
+
const validation = (0, import_core.validateTitle)(title);
|
|
21
51
|
if (!validation.valid) {
|
|
22
52
|
results.push({
|
|
23
53
|
id: "title-presence",
|
|
@@ -71,6 +101,9 @@ function checkTitle(input) {
|
|
|
71
101
|
}
|
|
72
102
|
return results;
|
|
73
103
|
}
|
|
104
|
+
|
|
105
|
+
// src/checks/meta-description.ts
|
|
106
|
+
var import_core2 = require("@power-seo/core");
|
|
74
107
|
function checkMetaDescription(input) {
|
|
75
108
|
const results = [];
|
|
76
109
|
const { metaDescription, focusKeyphrase } = input;
|
|
@@ -85,7 +118,7 @@ function checkMetaDescription(input) {
|
|
|
85
118
|
});
|
|
86
119
|
return results;
|
|
87
120
|
}
|
|
88
|
-
const validation =
|
|
121
|
+
const validation = (0, import_core2.validateMetaDescription)(metaDescription);
|
|
89
122
|
if (!validation.valid) {
|
|
90
123
|
results.push({
|
|
91
124
|
id: "meta-description-presence",
|
|
@@ -139,6 +172,9 @@ function checkMetaDescription(input) {
|
|
|
139
172
|
}
|
|
140
173
|
return results;
|
|
141
174
|
}
|
|
175
|
+
|
|
176
|
+
// src/checks/keyphrase-usage.ts
|
|
177
|
+
var import_core3 = require("@power-seo/core");
|
|
142
178
|
function checkKeyphraseUsage(input) {
|
|
143
179
|
const results = [];
|
|
144
180
|
const { focusKeyphrase, title, metaDescription, content, slug, images } = input;
|
|
@@ -153,7 +189,7 @@ function checkKeyphraseUsage(input) {
|
|
|
153
189
|
});
|
|
154
190
|
return results;
|
|
155
191
|
}
|
|
156
|
-
const occurrences =
|
|
192
|
+
const occurrences = (0, import_core3.analyzeKeyphraseOccurrences)({
|
|
157
193
|
keyphrase: focusKeyphrase,
|
|
158
194
|
title,
|
|
159
195
|
metaDescription,
|
|
@@ -161,27 +197,27 @@ function checkKeyphraseUsage(input) {
|
|
|
161
197
|
slug,
|
|
162
198
|
images
|
|
163
199
|
});
|
|
164
|
-
const densityResult =
|
|
165
|
-
if (densityResult.density <
|
|
200
|
+
const densityResult = (0, import_core3.calculateKeywordDensity)(focusKeyphrase, content);
|
|
201
|
+
if (densityResult.density < import_core3.KEYWORD_DENSITY.MIN) {
|
|
166
202
|
results.push({
|
|
167
203
|
id: "keyphrase-density",
|
|
168
204
|
title: "Keyphrase density",
|
|
169
|
-
description: `Keyphrase density is ${densityResult.density}%, which is below the recommended minimum of ${
|
|
205
|
+
description: `Keyphrase density is ${densityResult.density}%, which is below the recommended minimum of ${import_core3.KEYWORD_DENSITY.MIN}%. Use the keyphrase more often.`,
|
|
170
206
|
status: "poor",
|
|
171
207
|
score: 1,
|
|
172
208
|
maxScore: 5
|
|
173
209
|
});
|
|
174
|
-
} else if (densityResult.density >
|
|
210
|
+
} else if (densityResult.density > import_core3.KEYWORD_DENSITY.MAX) {
|
|
175
211
|
results.push({
|
|
176
212
|
id: "keyphrase-density",
|
|
177
213
|
title: "Keyphrase density",
|
|
178
|
-
description: `Keyphrase density is ${densityResult.density}%, which exceeds the recommended maximum of ${
|
|
214
|
+
description: `Keyphrase density is ${densityResult.density}%, which exceeds the recommended maximum of ${import_core3.KEYWORD_DENSITY.MAX}%. Reduce usage to avoid keyword stuffing.`,
|
|
179
215
|
status: "poor",
|
|
180
216
|
score: 1,
|
|
181
217
|
maxScore: 5
|
|
182
218
|
});
|
|
183
|
-
} else if (densityResult.density >=
|
|
184
|
-
const isOptimal = Math.abs(densityResult.density -
|
|
219
|
+
} else if (densityResult.density >= import_core3.KEYWORD_DENSITY.MIN && densityResult.density <= import_core3.KEYWORD_DENSITY.MAX) {
|
|
220
|
+
const isOptimal = Math.abs(densityResult.density - import_core3.KEYWORD_DENSITY.OPTIMAL) < 0.5;
|
|
185
221
|
results.push({
|
|
186
222
|
id: "keyphrase-density",
|
|
187
223
|
title: "Keyphrase density",
|
|
@@ -227,15 +263,37 @@ function checkKeyphraseUsage(input) {
|
|
|
227
263
|
}
|
|
228
264
|
return results;
|
|
229
265
|
}
|
|
266
|
+
|
|
267
|
+
// src/checks/headings.ts
|
|
268
|
+
var import_core4 = require("@power-seo/core");
|
|
230
269
|
function parseHeadings(html) {
|
|
231
270
|
const headings = [];
|
|
232
|
-
const
|
|
233
|
-
let
|
|
234
|
-
while (
|
|
271
|
+
const lc = html.toLowerCase();
|
|
272
|
+
let pos = 0;
|
|
273
|
+
while (pos < lc.length) {
|
|
274
|
+
let earliest = -1;
|
|
275
|
+
let earliestLevel = 0;
|
|
276
|
+
for (let level = 1; level <= 6; level++) {
|
|
277
|
+
const idx = lc.indexOf(`<h${level}`, pos);
|
|
278
|
+
if (idx !== -1 && (earliest === -1 || idx < earliest)) {
|
|
279
|
+
earliest = idx;
|
|
280
|
+
earliestLevel = level;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (earliest === -1) break;
|
|
284
|
+
const contentStart = lc.indexOf(">", earliest);
|
|
285
|
+
if (contentStart === -1) break;
|
|
286
|
+
const closeTag = `</h${earliestLevel}>`;
|
|
287
|
+
const closeIdx = lc.indexOf(closeTag, contentStart + 1);
|
|
288
|
+
if (closeIdx === -1) {
|
|
289
|
+
pos = contentStart + 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
235
292
|
headings.push({
|
|
236
|
-
level:
|
|
237
|
-
text:
|
|
293
|
+
level: earliestLevel,
|
|
294
|
+
text: (0, import_core4.stripHtml)(html.slice(contentStart + 1, closeIdx))
|
|
238
295
|
});
|
|
296
|
+
pos = closeIdx + closeTag.length;
|
|
239
297
|
}
|
|
240
298
|
return headings;
|
|
241
299
|
}
|
|
@@ -327,24 +385,27 @@ function checkHeadings(input) {
|
|
|
327
385
|
}
|
|
328
386
|
return results;
|
|
329
387
|
}
|
|
388
|
+
|
|
389
|
+
// src/checks/word-count.ts
|
|
390
|
+
var import_core5 = require("@power-seo/core");
|
|
330
391
|
function checkWordCount(input) {
|
|
331
|
-
const words =
|
|
392
|
+
const words = (0, import_core5.getWords)(input.content);
|
|
332
393
|
const count = words.length;
|
|
333
|
-
if (count <
|
|
394
|
+
if (count < import_core5.MIN_WORD_COUNT) {
|
|
334
395
|
return {
|
|
335
396
|
id: "word-count",
|
|
336
397
|
title: "Word count",
|
|
337
|
-
description: `The content is ${count} words, which is below the recommended minimum of ${
|
|
398
|
+
description: `The content is ${count} words, which is below the recommended minimum of ${import_core5.MIN_WORD_COUNT}. Add more content to improve SEO.`,
|
|
338
399
|
status: "poor",
|
|
339
400
|
score: 1,
|
|
340
401
|
maxScore: 5
|
|
341
402
|
};
|
|
342
403
|
}
|
|
343
|
-
if (count <
|
|
404
|
+
if (count < import_core5.RECOMMENDED_WORD_COUNT) {
|
|
344
405
|
return {
|
|
345
406
|
id: "word-count",
|
|
346
407
|
title: "Word count",
|
|
347
|
-
description: `The content is ${count} words. Consider expanding to at least ${
|
|
408
|
+
description: `The content is ${count} words. Consider expanding to at least ${import_core5.RECOMMENDED_WORD_COUNT} words for more comprehensive coverage.`,
|
|
348
409
|
status: "ok",
|
|
349
410
|
score: 3,
|
|
350
411
|
maxScore: 5
|
|
@@ -512,14 +573,15 @@ function analyzeContent(input, config) {
|
|
|
512
573
|
recommendations
|
|
513
574
|
};
|
|
514
575
|
}
|
|
515
|
-
|
|
516
|
-
exports
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
576
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
577
|
+
0 && (module.exports = {
|
|
578
|
+
analyzeContent,
|
|
579
|
+
checkHeadings,
|
|
580
|
+
checkImages,
|
|
581
|
+
checkKeyphraseUsage,
|
|
582
|
+
checkLinks,
|
|
583
|
+
checkMetaDescription,
|
|
584
|
+
checkTitle,
|
|
585
|
+
checkWordCount
|
|
586
|
+
});
|
|
525
587
|
//# sourceMappingURL=index.cjs.map
|