@power-seo/content-analysis 1.0.6 → 1.0.7
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/README.md +100 -93
- 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 +3 -3
package/README.md
CHANGED
|
@@ -12,24 +12,24 @@ 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
|
|
|
21
21
|
## Why @power-seo/content-analysis?
|
|
22
22
|
|
|
23
|
-
|
|
|
24
|
-
|
|
25
|
-
| Keyphrase check
|
|
26
|
-
| Title validation
|
|
27
|
-
| Meta description
|
|
28
|
-
| Heading structure | ❌ Missed H1s
|
|
29
|
-
| Image alt text
|
|
30
|
-
| Link analysis
|
|
31
|
-
| SEO score
|
|
32
|
-
| Framework support | ❌ WordPress-only | ✅ Next.js, Remix, Vite, Node.js, Edge
|
|
23
|
+
| | Without | With |
|
|
24
|
+
| ----------------- | ----------------- | ------------------------------------------- |
|
|
25
|
+
| Keyphrase check | ❌ Manual grep | ✅ Density + distribution scoring |
|
|
26
|
+
| Title validation | ❌ Eye-check only | ✅ Presence, length, keyphrase match |
|
|
27
|
+
| Meta description | ❌ Unchecked | ✅ Length (120–160 chars) + keyphrase |
|
|
28
|
+
| Heading structure | ❌ Missed H1s | ✅ H1 hierarchy + keyphrase in subheadings |
|
|
29
|
+
| Image alt text | ❌ Skipped | ✅ Alt presence + keyphrase in alt |
|
|
30
|
+
| Link analysis | ❌ Unknown | ✅ Internal + external link presence |
|
|
31
|
+
| SEO score | ❌ Guesswork | ✅ Aggregate score with per-check breakdown |
|
|
32
|
+
| Framework support | ❌ WordPress-only | ✅ Next.js, Remix, Vite, Node.js, Edge |
|
|
33
33
|
|
|
34
34
|

|
|
35
35
|
|
|
@@ -60,20 +60,20 @@ Keyword-focused content analysis with real-time scoring, readability checks, and
|
|
|
60
60
|
|
|
61
61
|
| Feature | @power-seo/content-analysis | Yoast SEO | next-seo | seo-analyzer | react-helmet |
|
|
62
62
|
| ------------------------------ | :-------------------------: | :-------: | :------: | :----------: | :----------: |
|
|
63
|
-
| Keyphrase density check |
|
|
64
|
-
| Keyphrase distribution |
|
|
65
|
-
| Title + meta validation |
|
|
66
|
-
| Heading structure check |
|
|
67
|
-
| Image alt + keyphrase check |
|
|
68
|
-
| Internal / external link check |
|
|
69
|
-
| Aggregate SEO score |
|
|
70
|
-
| Per-check disable config |
|
|
71
|
-
| Works outside WordPress |
|
|
72
|
-
| TypeScript-first |
|
|
73
|
-
| Tree-shakeable |
|
|
74
|
-
| React UI components |
|
|
75
|
-
| CI / Node.js usage |
|
|
76
|
-
| Zero runtime dependencies |
|
|
63
|
+
| Keyphrase density check | ✅ | ✅ | ❌ | Partial | ❌ |
|
|
64
|
+
| Keyphrase distribution | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
65
|
+
| Title + meta validation | ✅ | ✅ | ❌ | Partial | ❌ |
|
|
66
|
+
| Heading structure check | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
67
|
+
| Image alt + keyphrase check | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
68
|
+
| Internal / external link check | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
69
|
+
| Aggregate SEO score | ✅ | ✅ | ❌ | Partial | ❌ |
|
|
70
|
+
| Per-check disable config | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
71
|
+
| Works outside WordPress | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
72
|
+
| TypeScript-first | ✅ | ❌ | Partial | ❌ | ❌ |
|
|
73
|
+
| Tree-shakeable | ✅ | ❌ | Partial | ❌ | ❌ |
|
|
74
|
+
| React UI components | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
75
|
+
| CI / Node.js usage | ✅ | ❌ | ❌ | ✅ | ❌ |
|
|
76
|
+
| Zero runtime dependencies | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
77
77
|
|
|
78
78
|

|
|
79
79
|
|
|
@@ -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,22 @@ 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
120
|
- `good` — check fully passes
|
|
120
|
-
- `
|
|
121
|
+
- `ok` — check partially passes
|
|
121
122
|
- `poor` — check fails
|
|
122
123
|
|
|
123
124
|
---
|
|
@@ -133,17 +134,18 @@ import { analyzeContent } from '@power-seo/content-analysis';
|
|
|
133
134
|
|
|
134
135
|
const output = analyzeContent({
|
|
135
136
|
title: 'Next.js SEO Best Practices',
|
|
136
|
-
metaDescription:
|
|
137
|
-
|
|
137
|
+
metaDescription:
|
|
138
|
+
'Learn how to optimize your Next.js app for search engines with meta tags and structured data.',
|
|
139
|
+
focusKeyphrase: 'next.js seo',
|
|
138
140
|
content: htmlString,
|
|
139
|
-
url: 'https://example.com/nextjs-seo',
|
|
140
141
|
images: imageList,
|
|
141
142
|
internalLinks: internalLinks,
|
|
142
143
|
externalLinks: externalLinks,
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
// output.
|
|
146
|
-
// output.
|
|
146
|
+
// output.score → number (sum of all check scores)
|
|
147
|
+
// output.maxScore → number (maximum possible score)
|
|
148
|
+
// output.results → AnalysisResult[]
|
|
147
149
|
```
|
|
148
150
|
|
|
149
151
|
### Running Individual Checks
|
|
@@ -162,20 +164,22 @@ import {
|
|
|
162
164
|
} from '@power-seo/content-analysis';
|
|
163
165
|
|
|
164
166
|
const titleResults = checkTitle({
|
|
165
|
-
title: 'React SEO Guide',
|
|
166
|
-
keyphrase: 'react seo',
|
|
167
167
|
content: '',
|
|
168
|
+
title: 'React SEO Guide',
|
|
169
|
+
focusKeyphrase: 'react seo',
|
|
168
170
|
});
|
|
169
|
-
// [
|
|
170
|
-
//
|
|
171
|
+
// [
|
|
172
|
+
// { id: 'title-presence', title: 'SEO title', description: '...', status: 'good', score: 5, maxScore: 5 },
|
|
173
|
+
// { id: 'title-keyphrase', title: 'Keyphrase in title', description: '...', status: 'good', score: 5, maxScore: 5 }
|
|
174
|
+
// ]
|
|
171
175
|
|
|
172
176
|
const wcResult = checkWordCount({ content: shortHtml });
|
|
173
|
-
// { id: 'word-count',
|
|
177
|
+
// { id: 'word-count', title: 'Word count', description: 'The content is 180 words...', status: 'poor', score: 1, maxScore: 5 }
|
|
174
178
|
```
|
|
175
179
|
|
|
176
180
|
### Disabling Specific Checks
|
|
177
181
|
|
|
178
|
-
Pass `config.disabledChecks` to
|
|
182
|
+
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
183
|
|
|
180
184
|
```ts
|
|
181
185
|
import { analyzeContent } from '@power-seo/content-analysis';
|
|
@@ -191,6 +195,7 @@ Import from the `/react` entry point for pre-built analysis UI components:
|
|
|
191
195
|
|
|
192
196
|
```tsx
|
|
193
197
|
import { ContentAnalyzer, ScorePanel, CheckList } from '@power-seo/content-analysis/react';
|
|
198
|
+
import { analyzeContent } from '@power-seo/content-analysis';
|
|
194
199
|
import type { ContentAnalysisInput } from '@power-seo/content-analysis';
|
|
195
200
|
|
|
196
201
|
// All-in-one component
|
|
@@ -217,13 +222,13 @@ Block deploys when SEO checks fail:
|
|
|
217
222
|
```ts
|
|
218
223
|
import { analyzeContent } from '@power-seo/content-analysis';
|
|
219
224
|
|
|
220
|
-
const output = analyzeContent({ title, metaDescription,
|
|
225
|
+
const output = analyzeContent({ title, metaDescription, focusKeyphrase, content });
|
|
221
226
|
|
|
222
227
|
const failures = output.results.filter((r) => r.status === 'poor');
|
|
223
228
|
|
|
224
229
|
if (failures.length > 0) {
|
|
225
230
|
console.error('SEO checks failed:');
|
|
226
|
-
failures.forEach((r) => console.error(' ✗', r.
|
|
231
|
+
failures.forEach((r) => console.error(' ✗', r.description));
|
|
227
232
|
process.exit(1);
|
|
228
233
|
}
|
|
229
234
|
```
|
|
@@ -234,10 +239,10 @@ if (failures.length > 0) {
|
|
|
234
239
|
|
|
235
240
|
### Entry Points
|
|
236
241
|
|
|
237
|
-
| Import
|
|
238
|
-
|
|
|
239
|
-
| `@power-seo/content-analysis`
|
|
240
|
-
| `@power-seo/content-analysis/react` | React components for analysis UI
|
|
242
|
+
| Import | Description |
|
|
243
|
+
| ----------------------------------- | -------------------------------------------- |
|
|
244
|
+
| `@power-seo/content-analysis` | Core analyzer and individual check functions |
|
|
245
|
+
| `@power-seo/content-analysis/react` | React components for analysis UI |
|
|
241
246
|
|
|
242
247
|
### `analyzeContent()`
|
|
243
248
|
|
|
@@ -250,36 +255,36 @@ function analyzeContent(
|
|
|
250
255
|
|
|
251
256
|
#### `ContentAnalysisInput`
|
|
252
257
|
|
|
253
|
-
| Prop | Type | Required | Description
|
|
254
|
-
| ----------------- | -------------------------------------- | -------- |
|
|
255
|
-
| `content` | `string` | ✅ | Body HTML string
|
|
256
|
-
| `title` | `string` | — | Page `<title>` content
|
|
257
|
-
| `metaDescription` | `string` | — | Meta description content
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `images` | `Array<{ src: string; alt?: string }>` | — | Images found on the page
|
|
261
|
-
| `internalLinks` | `string[]` | — | Internal link URLs
|
|
262
|
-
| `externalLinks` | `string[]` | — | External link URLs
|
|
258
|
+
| Prop | Type | Required | Description |
|
|
259
|
+
| ----------------- | -------------------------------------- | -------- | ------------------------------------------- |
|
|
260
|
+
| `content` | `string` | ✅ | Body HTML string |
|
|
261
|
+
| `title` | `string` | — | Page `<title>` content |
|
|
262
|
+
| `metaDescription` | `string` | — | Meta description content |
|
|
263
|
+
| `focusKeyphrase` | `string` | — | Focus keyphrase to analyze against |
|
|
264
|
+
| `slug` | `string` | — | URL slug (used for keyphrase-in-slug check) |
|
|
265
|
+
| `images` | `Array<{ src: string; alt?: string }>` | — | Images found on the page |
|
|
266
|
+
| `internalLinks` | `string[]` | — | Internal link URLs |
|
|
267
|
+
| `externalLinks` | `string[]` | — | External link URLs |
|
|
263
268
|
|
|
264
269
|
#### `ContentAnalysisOutput`
|
|
265
270
|
|
|
266
|
-
| Field
|
|
267
|
-
|
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
272
|
-
| `recommendations` | `string[]` | Descriptions from all failed or partial checks |
|
|
271
|
+
| Field | Type | Description |
|
|
272
|
+
| ----------------- | ------------------ | ------------------------------------------------- |
|
|
273
|
+
| `score` | `number` | Sum of all individual check scores |
|
|
274
|
+
| `maxScore` | `number` | Maximum possible score (varies by enabled checks) |
|
|
275
|
+
| `results` | `AnalysisResult[]` | Per-check results |
|
|
276
|
+
| `recommendations` | `string[]` | Descriptions from all failed or partial checks |
|
|
273
277
|
|
|
274
278
|
#### `AnalysisResult`
|
|
275
279
|
|
|
276
|
-
| Field
|
|
277
|
-
|
|
|
278
|
-
| `id`
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
280
|
+
| Field | Type | Description |
|
|
281
|
+
| ------------- | ---------------- | ------------------------------------------------------ |
|
|
282
|
+
| `id` | `string` | Unique check identifier (one of the `CheckId` values) |
|
|
283
|
+
| `title` | `string` | Short display label for the check (e.g. `"SEO title"`) |
|
|
284
|
+
| `description` | `string` | Human-readable actionable feedback |
|
|
285
|
+
| `status` | `AnalysisStatus` | `'good'` \| `'ok'` \| `'poor'` |
|
|
286
|
+
| `score` | `number` | Points earned for this check |
|
|
287
|
+
| `maxScore` | `number` | Maximum points for this check |
|
|
283
288
|
|
|
284
289
|
#### `AnalysisConfig`
|
|
285
290
|
|
|
@@ -289,26 +294,28 @@ function analyzeContent(
|
|
|
289
294
|
|
|
290
295
|
### Individual Check Functions
|
|
291
296
|
|
|
292
|
-
| Function | Check ID(s)
|
|
293
|
-
| ----------------------------- |
|
|
294
|
-
| `checkTitle(input)` | `title-presence`, `title-keyphrase`
|
|
295
|
-
| `checkMetaDescription(input)` | `meta-description-presence`, `meta-description-keyphrase`
|
|
296
|
-
| `checkKeyphraseUsage(input)` | `keyphrase-density`, `keyphrase-distribution`
|
|
297
|
-
| `checkHeadings(input)` | `heading-structure`, `heading-keyphrase`
|
|
298
|
-
| `checkWordCount(input)` | `word-count`
|
|
299
|
-
| `checkImages(input)` | `image-alt`, `image-keyphrase`
|
|
300
|
-
| `checkLinks(input)` | `internal-links`, `external-links`
|
|
297
|
+
| Function | Check ID(s) | Checks For |
|
|
298
|
+
| ----------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
299
|
+
| `checkTitle(input)` | `title-presence`, `title-keyphrase` | Title presence **and** length (50–60 chars, validated inside `title-presence`), keyphrase |
|
|
300
|
+
| `checkMetaDescription(input)` | `meta-description-presence`, `meta-description-keyphrase` | Description presence, length (120–160 chars), keyphrase |
|
|
301
|
+
| `checkKeyphraseUsage(input)` | `keyphrase-density`, `keyphrase-distribution` | Density (0.5–2.5%) and occurrence in key areas |
|
|
302
|
+
| `checkHeadings(input)` | `heading-structure`, `heading-keyphrase` | H1 presence, hierarchy, keyphrase in subheadings |
|
|
303
|
+
| `checkWordCount(input)` | `word-count` | Min 300 words (good at 1,000+) |
|
|
304
|
+
| `checkImages(input)` | `image-alt`, `image-keyphrase` | Alt text presence and keyphrase in alt |
|
|
305
|
+
| `checkLinks(input)` | `internal-links`, `external-links` | Internal and external link presence |
|
|
306
|
+
|
|
307
|
+
> **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
308
|
|
|
302
309
|
### Types
|
|
303
310
|
|
|
304
|
-
| Type | Description
|
|
305
|
-
| ----------------------- |
|
|
306
|
-
| `CheckId` | Union of all 13 built-in check IDs
|
|
307
|
-
| `AnalysisConfig` | `{ disabledChecks?: CheckId[] }`
|
|
308
|
-
| `AnalysisStatus` | `'good' \| '
|
|
309
|
-
| `ContentAnalysisInput` | Input shape for `analyzeContent()`
|
|
310
|
-
| `ContentAnalysisOutput` | Output shape from `analyzeContent()`
|
|
311
|
-
| `AnalysisResult` | Single check result with id, status,
|
|
311
|
+
| Type | Description |
|
|
312
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
313
|
+
| `CheckId` | Union of all 13 built-in check IDs |
|
|
314
|
+
| `AnalysisConfig` | `{ disabledChecks?: CheckId[] }` |
|
|
315
|
+
| `AnalysisStatus` | `'good' \| 'ok' \| 'poor'` |
|
|
316
|
+
| `ContentAnalysisInput` | Input shape for `analyzeContent()` |
|
|
317
|
+
| `ContentAnalysisOutput` | Output shape from `analyzeContent()` |
|
|
318
|
+
| `AnalysisResult` | Single check result with id, title, description, status, score, maxScore |
|
|
312
319
|
|
|
313
320
|
---
|
|
314
321
|
|
|
@@ -326,7 +333,7 @@ function analyzeContent(
|
|
|
326
333
|
## Architecture Overview
|
|
327
334
|
|
|
328
335
|
- **Pure TypeScript** — no compiled binary, no native modules
|
|
329
|
-
- **Zero runtime dependencies** —
|
|
336
|
+
- **Zero external runtime dependencies** — `@power-seo/core` ships as a direct dependency, no separate install
|
|
330
337
|
- **Framework-agnostic** — works in any JavaScript environment
|
|
331
338
|
- **SSR compatible** — safe to run in Next.js Server Components, Remix loaders, or Express handlers
|
|
332
339
|
- **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
|