serpx-seo-meta-check 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/LICENSE +21 -0
- package/README.md +187 -0
- package/cli.js +287 -0
- package/index.js +294 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SerpX
|
|
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
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# SerpX SEO Meta Check
|
|
2
|
+
|
|
3
|
+
A fast, lightweight and zero-dependency command-line SEO checker for inspecting essential on-page and technical SEO metadata.
|
|
4
|
+
|
|
5
|
+
SerpX SEO Meta Check is built by [SerpX](https://serpx.ai/), an AI-powered SEO platform for website analysis, keyword research, competitor intelligence and content optimization.
|
|
6
|
+
|
|
7
|
+
## What It Checks
|
|
8
|
+
|
|
9
|
+
The package analyzes a public webpage and reports:
|
|
10
|
+
|
|
11
|
+
- Page title
|
|
12
|
+
- Title length
|
|
13
|
+
- Meta description
|
|
14
|
+
- Meta description length
|
|
15
|
+
- Canonical URL
|
|
16
|
+
- Meta robots directives
|
|
17
|
+
- Noindex detection
|
|
18
|
+
- HTML language
|
|
19
|
+
- Viewport configuration
|
|
20
|
+
- H1 count and content
|
|
21
|
+
- H2 count
|
|
22
|
+
- H3 count
|
|
23
|
+
- Approximate visible word count
|
|
24
|
+
- Open Graph title
|
|
25
|
+
- Open Graph description
|
|
26
|
+
- Open Graph image
|
|
27
|
+
- Twitter Card metadata
|
|
28
|
+
- JSON-LD structured data count
|
|
29
|
+
- Basic SEO score
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install globally:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g serpx-seo-meta-check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then analyze a page:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
serpx-seo-check https://example.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also run the package directly:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx serpx-seo-meta-check https://example.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Example Output
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
SerpX SEO Meta Check
|
|
55
|
+
========================================
|
|
56
|
+
Final URL: https://example.com/
|
|
57
|
+
Basic SEO score: 45/100
|
|
58
|
+
|
|
59
|
+
Core Metadata
|
|
60
|
+
----------------------------------------
|
|
61
|
+
Title: Example Domain
|
|
62
|
+
Title length: 14
|
|
63
|
+
Meta description: Not found
|
|
64
|
+
Description length: 0
|
|
65
|
+
Canonical: Not found
|
|
66
|
+
Robots: Not found
|
|
67
|
+
Noindex: No
|
|
68
|
+
|
|
69
|
+
Page Structure
|
|
70
|
+
----------------------------------------
|
|
71
|
+
HTML lang: en
|
|
72
|
+
Viewport: Found
|
|
73
|
+
H1 count: 1
|
|
74
|
+
H1: Example Domain
|
|
75
|
+
H2 count: 0
|
|
76
|
+
H3 count: 0
|
|
77
|
+
Approx. word count: 28
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Why Use SerpX SEO Meta Check?
|
|
81
|
+
|
|
82
|
+
Small technical SEO mistakes can create large search visibility problems.
|
|
83
|
+
|
|
84
|
+
A missing canonical tag, an accidental noindex directive, weak metadata or incorrect heading structure can affect how search engines understand a webpage.
|
|
85
|
+
|
|
86
|
+
SerpX SEO Meta Check provides a fast first-pass SEO inspection directly from the terminal.
|
|
87
|
+
|
|
88
|
+
For deeper website analysis, SEO audits, keyword intelligence and AI-powered optimization, visit the [SerpX AI SEO Platform](https://serpx.ai/).
|
|
89
|
+
|
|
90
|
+
## Competitor Keyword Research
|
|
91
|
+
|
|
92
|
+
On-page metadata is only one part of a complete SEO strategy.
|
|
93
|
+
|
|
94
|
+
Competitive keyword research can reveal the search queries competing websites rank for, identify content gaps and uncover new organic search opportunities.
|
|
95
|
+
|
|
96
|
+
Read the detailed SerpX guide:
|
|
97
|
+
|
|
98
|
+
[Competitor Keyword Research: How to Find and Analyze Competitor Keywords](https://serpx.ai/blog/competitor-keyword-research)
|
|
99
|
+
|
|
100
|
+
The guide explains practical competitor keyword research workflows and how competitive search data can be converted into an actionable SEO strategy.
|
|
101
|
+
|
|
102
|
+
## Programmatic Usage
|
|
103
|
+
|
|
104
|
+
The analyzer can also be imported into a Node.js project:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { analyzeHtml } from "serpx-seo-meta-check";
|
|
108
|
+
|
|
109
|
+
const html = `
|
|
110
|
+
<!doctype html>
|
|
111
|
+
<html lang="en">
|
|
112
|
+
<head>
|
|
113
|
+
<title>Example Page</title>
|
|
114
|
+
<meta
|
|
115
|
+
name="description"
|
|
116
|
+
content="Example page description"
|
|
117
|
+
>
|
|
118
|
+
<link
|
|
119
|
+
rel="canonical"
|
|
120
|
+
href="https://example.com/"
|
|
121
|
+
>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<h1>Example Page</h1>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const result = analyzeHtml(html);
|
|
130
|
+
|
|
131
|
+
console.log(result);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## SEO Resources
|
|
135
|
+
|
|
136
|
+
Useful SerpX resources:
|
|
137
|
+
|
|
138
|
+
- [SerpX AI SEO Platform](https://serpx.ai/)
|
|
139
|
+
- [Competitor Keyword Research Guide](https://serpx.ai/blog/competitor-keyword-research)
|
|
140
|
+
|
|
141
|
+
## Requirements
|
|
142
|
+
|
|
143
|
+
- Node.js 18 or newer
|
|
144
|
+
- Public HTTP or HTTPS URL
|
|
145
|
+
|
|
146
|
+
The package does not require third-party npm dependencies.
|
|
147
|
+
|
|
148
|
+
## Privacy
|
|
149
|
+
|
|
150
|
+
The CLI requests only the URL supplied by the user in order to analyze its publicly available HTML.
|
|
151
|
+
|
|
152
|
+
It does not require:
|
|
153
|
+
|
|
154
|
+
- A SerpX account
|
|
155
|
+
- A SerpX API key
|
|
156
|
+
- Analytics configuration
|
|
157
|
+
- Tracking software
|
|
158
|
+
|
|
159
|
+
## Limitations
|
|
160
|
+
|
|
161
|
+
This package performs static analysis of the HTML returned by the target server.
|
|
162
|
+
|
|
163
|
+
Websites that insert SEO metadata only after client-side JavaScript execution may require browser rendering for a complete audit.
|
|
164
|
+
|
|
165
|
+
The SEO score is intended as a quick diagnostic indicator and is not a search-engine ranking score.
|
|
166
|
+
|
|
167
|
+
## About SerpX
|
|
168
|
+
|
|
169
|
+
[SerpX](https://serpx.ai/) is an AI-powered SEO and content optimization platform built for website owners, developers, marketers and SEO professionals.
|
|
170
|
+
|
|
171
|
+
SerpX provides tools for website analysis, technical SEO, content optimization, competitor intelligence and keyword research.
|
|
172
|
+
|
|
173
|
+
For competitive organic search analysis, read the [SerpX competitor keyword research guide](https://serpx.ai/blog/competitor-keyword-research).
|
|
174
|
+
|
|
175
|
+
## Links
|
|
176
|
+
|
|
177
|
+
Website:
|
|
178
|
+
|
|
179
|
+
https://serpx.ai/
|
|
180
|
+
|
|
181
|
+
Competitor Keyword Research:
|
|
182
|
+
|
|
183
|
+
https://serpx.ai/blog/competitor-keyword-research
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { analyzeHtml } from "./index.js";
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
function showHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
SerpX SEO Meta Check
|
|
10
|
+
|
|
11
|
+
Fast command-line on-page SEO metadata analyzer.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
serpx-seo-check <url>
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
serpx-seo-check https://example.com
|
|
18
|
+
npx serpx-seo-meta-check https://example.com
|
|
19
|
+
|
|
20
|
+
Checks:
|
|
21
|
+
- Page title
|
|
22
|
+
- Title length
|
|
23
|
+
- Meta description
|
|
24
|
+
- Meta description length
|
|
25
|
+
- Canonical URL
|
|
26
|
+
- Robots directives
|
|
27
|
+
- Noindex status
|
|
28
|
+
- H1 headings
|
|
29
|
+
- H2 and H3 counts
|
|
30
|
+
- HTML language
|
|
31
|
+
- Viewport
|
|
32
|
+
- Open Graph metadata
|
|
33
|
+
- Twitter Card
|
|
34
|
+
- JSON-LD structured data
|
|
35
|
+
- Approximate visible word count
|
|
36
|
+
- Basic SEO score
|
|
37
|
+
|
|
38
|
+
More SEO tools:
|
|
39
|
+
https://serpx.ai/
|
|
40
|
+
|
|
41
|
+
SEO research:
|
|
42
|
+
https://serpx.ai/blog/competitor-keyword-research
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
args.length === 0 ||
|
|
48
|
+
args.includes("--help") ||
|
|
49
|
+
args.includes("-h")
|
|
50
|
+
) {
|
|
51
|
+
showHelp();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
args.includes("--version") ||
|
|
57
|
+
args.includes("-v")
|
|
58
|
+
) {
|
|
59
|
+
console.log("1.0.0");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const input = args[0];
|
|
64
|
+
|
|
65
|
+
let parsedUrl;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
parsedUrl = new URL(input);
|
|
69
|
+
} catch {
|
|
70
|
+
console.error("Error: Invalid URL.");
|
|
71
|
+
console.error(
|
|
72
|
+
"Example: serpx-seo-check https://example.com"
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
!["http:", "https:"].includes(
|
|
79
|
+
parsedUrl.protocol
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
console.error(
|
|
83
|
+
"Error: Only HTTP and HTTPS URLs are supported."
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(parsedUrl, {
|
|
90
|
+
redirect: "follow",
|
|
91
|
+
signal: AbortSignal.timeout(15000),
|
|
92
|
+
headers: {
|
|
93
|
+
"user-agent":
|
|
94
|
+
"SerpX-SEO-Meta-Check/1.0 (+https://serpx.ai/)",
|
|
95
|
+
"accept":
|
|
96
|
+
"text/html,application/xhtml+xml"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`HTTP ${response.status} ${response.statusText}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const contentType =
|
|
107
|
+
response.headers.get("content-type") || "";
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
contentType &&
|
|
111
|
+
!contentType.includes("text/html") &&
|
|
112
|
+
!contentType.includes(
|
|
113
|
+
"application/xhtml+xml"
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Unsupported content type: ${contentType}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const html = await response.text();
|
|
122
|
+
|
|
123
|
+
const result = analyzeHtml(html);
|
|
124
|
+
|
|
125
|
+
console.log("");
|
|
126
|
+
console.log("SerpX SEO Meta Check");
|
|
127
|
+
console.log(
|
|
128
|
+
"========================================"
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
console.log(
|
|
132
|
+
`Final URL: ${response.url}`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`Basic SEO score: ${result.score}/100`
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
console.log("");
|
|
140
|
+
console.log("Core Metadata");
|
|
141
|
+
console.log(
|
|
142
|
+
"----------------------------------------"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
console.log(
|
|
146
|
+
`Title: ${
|
|
147
|
+
result.title || "Not found"
|
|
148
|
+
}`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`Title length: ${result.titleLength}`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
`Meta description: ${
|
|
157
|
+
result.metaDescription || "Not found"
|
|
158
|
+
}`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
console.log(
|
|
162
|
+
`Description length: ${result.metaDescriptionLength}`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
console.log(
|
|
166
|
+
`Canonical: ${
|
|
167
|
+
result.canonical || "Not found"
|
|
168
|
+
}`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
console.log(
|
|
172
|
+
`Robots: ${
|
|
173
|
+
result.robots || "Not found"
|
|
174
|
+
}`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
console.log(
|
|
178
|
+
`Noindex: ${
|
|
179
|
+
result.noindex ? "Yes" : "No"
|
|
180
|
+
}`
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log("Page Structure");
|
|
185
|
+
console.log(
|
|
186
|
+
"----------------------------------------"
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
console.log(
|
|
190
|
+
`HTML lang: ${
|
|
191
|
+
result.lang || "Not found"
|
|
192
|
+
}`
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
console.log(
|
|
196
|
+
`Viewport: ${
|
|
197
|
+
result.viewport ? "Found" : "Not found"
|
|
198
|
+
}`
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
console.log(
|
|
202
|
+
`H1 count: ${result.h1Count}`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
console.log(
|
|
206
|
+
`H1: ${
|
|
207
|
+
result.h1.join(" | ") || "Not found"
|
|
208
|
+
}`
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
console.log(
|
|
212
|
+
`H2 count: ${result.h2Count}`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
console.log(
|
|
216
|
+
`H3 count: ${result.h3Count}`
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
console.log(
|
|
220
|
+
`Approx. word count: ${result.wordCount}`
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log(
|
|
225
|
+
"Social & Structured Data"
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
console.log(
|
|
229
|
+
"----------------------------------------"
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
console.log(
|
|
233
|
+
`Open Graph title: ${
|
|
234
|
+
result.ogTitle || "Not found"
|
|
235
|
+
}`
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
console.log(
|
|
239
|
+
`Open Graph description: ${
|
|
240
|
+
result.ogDescription || "Not found"
|
|
241
|
+
}`
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
console.log(
|
|
245
|
+
`Open Graph image: ${
|
|
246
|
+
result.ogImage || "Not found"
|
|
247
|
+
}`
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
console.log(
|
|
251
|
+
`Twitter Card: ${
|
|
252
|
+
result.twitterCard || "Not found"
|
|
253
|
+
}`
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
console.log(
|
|
257
|
+
`JSON-LD blocks: ${result.jsonLdCount}`
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log("More SEO tools:");
|
|
262
|
+
console.log("https://serpx.ai/");
|
|
263
|
+
|
|
264
|
+
console.log("");
|
|
265
|
+
console.log(
|
|
266
|
+
"Competitor keyword research:"
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
console.log(
|
|
270
|
+
"https://serpx.ai/blog/competitor-keyword-research"
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
console.log("");
|
|
274
|
+
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error?.name === "TimeoutError") {
|
|
277
|
+
console.error(
|
|
278
|
+
"Failed to analyze URL: Request timed out."
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
console.error(
|
|
282
|
+
`Failed to analyze URL: ${error.message}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
function decodeHtml(value = "") {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/&#(\d+);/g, (_, code) =>
|
|
4
|
+
String.fromCodePoint(Number(code))
|
|
5
|
+
)
|
|
6
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, code) =>
|
|
7
|
+
String.fromCodePoint(parseInt(code, 16))
|
|
8
|
+
)
|
|
9
|
+
.replace(/ /gi, " ")
|
|
10
|
+
.replace(/&/gi, "&")
|
|
11
|
+
.replace(/"/gi, '"')
|
|
12
|
+
.replace(/'/gi, "'")
|
|
13
|
+
.replace(/</gi, "<")
|
|
14
|
+
.replace(/>/gi, ">");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanText(value = "") {
|
|
18
|
+
return decodeHtml(
|
|
19
|
+
value
|
|
20
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
21
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
|
22
|
+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, " ")
|
|
23
|
+
.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, " ")
|
|
24
|
+
.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, " ")
|
|
25
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
26
|
+
.replace(/<[^>]+>/g, " ")
|
|
27
|
+
)
|
|
28
|
+
.replace(/\s+/g, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getAttributes(tag = "") {
|
|
33
|
+
const attributes = {};
|
|
34
|
+
|
|
35
|
+
const pattern =
|
|
36
|
+
/([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
37
|
+
|
|
38
|
+
let match;
|
|
39
|
+
|
|
40
|
+
while ((match = pattern.exec(tag)) !== null) {
|
|
41
|
+
const key = match[1].toLowerCase();
|
|
42
|
+
|
|
43
|
+
if (key.startsWith("<")) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
attributes[key] = decodeHtml(
|
|
48
|
+
(match[2] ?? match[3] ?? match[4] ?? "").trim()
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return attributes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findMeta(html, attribute, expectedValue) {
|
|
56
|
+
const tags = html.match(/<meta\b[^>]*>/gi) || [];
|
|
57
|
+
|
|
58
|
+
for (const tag of tags) {
|
|
59
|
+
const attrs = getAttributes(tag);
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
attrs[attribute] &&
|
|
63
|
+
attrs[attribute].toLowerCase() === expectedValue.toLowerCase()
|
|
64
|
+
) {
|
|
65
|
+
return attrs.content || "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findLink(html, relValue) {
|
|
73
|
+
const tags = html.match(/<link\b[^>]*>/gi) || [];
|
|
74
|
+
|
|
75
|
+
for (const tag of tags) {
|
|
76
|
+
const attrs = getAttributes(tag);
|
|
77
|
+
|
|
78
|
+
const rel = (attrs.rel || "")
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.split(/\s+/)
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
|
|
83
|
+
if (rel.includes(relValue.toLowerCase())) {
|
|
84
|
+
return attrs.href || "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findHtmlLang(html) {
|
|
92
|
+
const match = html.match(/<html\b[^>]*>/i);
|
|
93
|
+
|
|
94
|
+
if (!match) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return getAttributes(match[0]).lang || "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getTitle(html) {
|
|
102
|
+
const match = html.match(
|
|
103
|
+
/<title\b[^>]*>([\s\S]*?)<\/title>/i
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return match ? cleanText(match[1]) : "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getHeadings(html, level) {
|
|
110
|
+
let pattern;
|
|
111
|
+
|
|
112
|
+
if (level === 1) {
|
|
113
|
+
pattern = /<h1(?:\s[^>]*)?>([\s\S]*?)<\/h1>/gi;
|
|
114
|
+
} else if (level === 2) {
|
|
115
|
+
pattern = /<h2(?:\s[^>]*)?>([\s\S]*?)<\/h2>/gi;
|
|
116
|
+
} else if (level === 3) {
|
|
117
|
+
pattern = /<h3(?:\s[^>]*)?>([\s\S]*?)<\/h3>/gi;
|
|
118
|
+
} else {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [...html.matchAll(pattern)]
|
|
123
|
+
.map(match => cleanText(match[1]))
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
function countJsonLd(html) {
|
|
129
|
+
const matches =
|
|
130
|
+
html.match(
|
|
131
|
+
/<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi
|
|
132
|
+
) || [];
|
|
133
|
+
|
|
134
|
+
return matches.length;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function calculateScore(result) {
|
|
138
|
+
let score = 0;
|
|
139
|
+
|
|
140
|
+
if (result.title) {
|
|
141
|
+
score += 15;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
result.titleLength >= 20 &&
|
|
146
|
+
result.titleLength <= 65
|
|
147
|
+
) {
|
|
148
|
+
score += 10;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.metaDescription) {
|
|
152
|
+
score += 15;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
result.metaDescriptionLength >= 70 &&
|
|
157
|
+
result.metaDescriptionLength <= 170
|
|
158
|
+
) {
|
|
159
|
+
score += 10;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result.canonical) {
|
|
163
|
+
score += 10;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (result.h1Count === 1) {
|
|
167
|
+
score += 15;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (result.viewport) {
|
|
171
|
+
score += 5;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result.lang) {
|
|
175
|
+
score += 5;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!result.noindex) {
|
|
179
|
+
score += 5;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (result.jsonLdCount > 0) {
|
|
183
|
+
score += 5;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (result.ogTitle) {
|
|
187
|
+
score += 3;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (result.ogDescription) {
|
|
191
|
+
score += 2;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Math.min(score, 100);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function analyzeHtml(html = "") {
|
|
198
|
+
if (typeof html !== "string") {
|
|
199
|
+
throw new TypeError("HTML input must be a string.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const title = getTitle(html);
|
|
203
|
+
|
|
204
|
+
const metaDescription =
|
|
205
|
+
findMeta(html, "name", "description") ||
|
|
206
|
+
findMeta(html, "property", "description");
|
|
207
|
+
|
|
208
|
+
const canonical = findLink(html, "canonical");
|
|
209
|
+
|
|
210
|
+
const robots = findMeta(
|
|
211
|
+
html,
|
|
212
|
+
"name",
|
|
213
|
+
"robots"
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const viewport = findMeta(
|
|
217
|
+
html,
|
|
218
|
+
"name",
|
|
219
|
+
"viewport"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const h1 = getHeadings(html, 1);
|
|
223
|
+
const h2 = getHeadings(html, 2);
|
|
224
|
+
const h3 = getHeadings(html, 3);
|
|
225
|
+
|
|
226
|
+
const visibleText = cleanText(html);
|
|
227
|
+
|
|
228
|
+
const wordCount = visibleText
|
|
229
|
+
? visibleText.split(/\s+/).filter(Boolean).length
|
|
230
|
+
: 0;
|
|
231
|
+
|
|
232
|
+
const result = {
|
|
233
|
+
title,
|
|
234
|
+
titleLength: title.length,
|
|
235
|
+
|
|
236
|
+
metaDescription,
|
|
237
|
+
metaDescriptionLength: metaDescription.length,
|
|
238
|
+
|
|
239
|
+
canonical,
|
|
240
|
+
|
|
241
|
+
robots,
|
|
242
|
+
noindex: /\bnoindex\b/i.test(robots),
|
|
243
|
+
|
|
244
|
+
lang: findHtmlLang(html),
|
|
245
|
+
|
|
246
|
+
viewport,
|
|
247
|
+
|
|
248
|
+
h1Count: h1.length,
|
|
249
|
+
h1,
|
|
250
|
+
|
|
251
|
+
h2Count: h2.length,
|
|
252
|
+
h3Count: h3.length,
|
|
253
|
+
|
|
254
|
+
wordCount,
|
|
255
|
+
|
|
256
|
+
ogTitle: findMeta(
|
|
257
|
+
html,
|
|
258
|
+
"property",
|
|
259
|
+
"og:title"
|
|
260
|
+
),
|
|
261
|
+
|
|
262
|
+
ogDescription: findMeta(
|
|
263
|
+
html,
|
|
264
|
+
"property",
|
|
265
|
+
"og:description"
|
|
266
|
+
),
|
|
267
|
+
|
|
268
|
+
ogImage: findMeta(
|
|
269
|
+
html,
|
|
270
|
+
"property",
|
|
271
|
+
"og:image"
|
|
272
|
+
),
|
|
273
|
+
|
|
274
|
+
ogUrl: findMeta(
|
|
275
|
+
html,
|
|
276
|
+
"property",
|
|
277
|
+
"og:url"
|
|
278
|
+
),
|
|
279
|
+
|
|
280
|
+
twitterCard: findMeta(
|
|
281
|
+
html,
|
|
282
|
+
"name",
|
|
283
|
+
"twitter:card"
|
|
284
|
+
),
|
|
285
|
+
|
|
286
|
+
jsonLdCount: countJsonLd(html)
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
result.score = calculateScore(result);
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default analyzeHtml;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "serpx-seo-meta-check",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fast zero-dependency CLI for checking titles, meta descriptions, canonical tags, robots directives, headings and essential on-page SEO metadata.",
|
|
5
|
+
"homepage": "https://serpx.ai/",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"seo",
|
|
8
|
+
"seo-checker",
|
|
9
|
+
"seo-audit",
|
|
10
|
+
"on-page-seo",
|
|
11
|
+
"technical-seo",
|
|
12
|
+
"meta-tags",
|
|
13
|
+
"meta-description",
|
|
14
|
+
"canonical",
|
|
15
|
+
"robots",
|
|
16
|
+
"headings",
|
|
17
|
+
"h1",
|
|
18
|
+
"word-count",
|
|
19
|
+
"seo-cli",
|
|
20
|
+
"website-audit",
|
|
21
|
+
"serpx"
|
|
22
|
+
],
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "SerpX",
|
|
25
|
+
"url": "https://serpx.ai/"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./index.js"
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"serpx-seo-check": "cli.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"index.js",
|
|
38
|
+
"cli.js",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"check": "node --check index.js && node --check cli.js",
|
|
47
|
+
"test": "node cli.js https://example.com",
|
|
48
|
+
"prepublishOnly": "npm run check"
|
|
49
|
+
}
|
|
50
|
+
}
|