@rog0x/mcp-seo-tools 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/README.md +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/heading-checker.d.ts +15 -0
- package/dist/tools/heading-checker.d.ts.map +1 -0
- package/dist/tools/heading-checker.js +123 -0
- package/dist/tools/heading-checker.js.map +1 -0
- package/dist/tools/keyword-density.d.ts +22 -0
- package/dist/tools/keyword-density.d.ts.map +1 -0
- package/dist/tools/keyword-density.js +176 -0
- package/dist/tools/keyword-density.js.map +1 -0
- package/dist/tools/link-checker.d.ts +22 -0
- package/dist/tools/link-checker.d.ts.map +1 -0
- package/dist/tools/link-checker.js +171 -0
- package/dist/tools/link-checker.js.map +1 -0
- package/dist/tools/meta-analyzer.d.ts +27 -0
- package/dist/tools/meta-analyzer.d.ts.map +1 -0
- package/dist/tools/meta-analyzer.js +161 -0
- package/dist/tools/meta-analyzer.js.map +1 -0
- package/dist/tools/page-speed.d.ts +31 -0
- package/dist/tools/page-speed.d.ts.map +1 -0
- package/dist/tools/page-speed.js +180 -0
- package/dist/tools/page-speed.js.map +1 -0
- package/dist/tools/sitemap-parser.d.ts +29 -0
- package/dist/tools/sitemap-parser.d.ts.map +1 -0
- package/dist/tools/sitemap-parser.js +224 -0
- package/dist/tools/sitemap-parser.js.map +1 -0
- package/package.json +24 -0
- package/src/index.ts +199 -0
- package/src/tools/heading-checker.ts +109 -0
- package/src/tools/keyword-density.ts +180 -0
- package/src/tools/link-checker.ts +163 -0
- package/src/tools/meta-analyzer.ts +148 -0
- package/src/tools/page-speed.ts +190 -0
- package/src/tools/sitemap-parser.ts +230 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# mcp-seo-tools
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that gives AI agents a suite of SEO analysis tools. Analyze any public webpage for meta tags, heading structure, broken links, keyword density, page speed, and sitemap health — all without external API keys.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `seo_meta_analyze` | Audit meta tags, Open Graph, and Twitter Card tags with scoring |
|
|
10
|
+
| `seo_heading_check` | Validate H1-H6 hierarchy, detect skipped levels and duplicates |
|
|
11
|
+
| `seo_link_check` | Find broken links, redirects, and missing anchor text |
|
|
12
|
+
| `seo_keyword_density` | Measure keyword frequency and check target keyword placement |
|
|
13
|
+
| `seo_page_speed` | Measure TTFB, load time, HTML size, and resource counts |
|
|
14
|
+
| `seo_sitemap_parse` | Parse sitemap.xml, check freshness, duplicates, and compliance |
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 18+
|
|
19
|
+
- No API keys needed — all tools use direct HTTP requests and HTML parsing
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone <repo-url>
|
|
25
|
+
cd mcp-seo-tools
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage with Claude Code
|
|
31
|
+
|
|
32
|
+
Add to your Claude Code MCP configuration:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add seo-tools node D:/products/mcp-servers/mcp-seo-tools/dist/index.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage with Claude Desktop
|
|
39
|
+
|
|
40
|
+
Add to your `claude_desktop_config.json`:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"seo-tools": {
|
|
46
|
+
"command": "node",
|
|
47
|
+
"args": ["D:/products/mcp-servers/mcp-seo-tools/dist/index.js"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Tool Details
|
|
54
|
+
|
|
55
|
+
### seo_meta_analyze
|
|
56
|
+
|
|
57
|
+
Fetches a URL and inspects all meta-related elements. Checks title length (optimal: 30-60 chars), meta description length (optimal: 120-160 chars), canonical URL, viewport tag, language attribute, Open Graph completeness, and Twitter Card setup. Returns a 0-100 score.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Input: { "url": "https://example.com" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### seo_heading_check
|
|
64
|
+
|
|
65
|
+
Parses all headings on a page and builds a visual hierarchy tree. Flags: missing H1, multiple H1s, skipped heading levels, empty headings, overly long headings (>70 chars), and duplicate headings.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Input: { "url": "https://example.com" }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### seo_link_check
|
|
72
|
+
|
|
73
|
+
Extracts all anchor tags from a page, resolves relative URLs, deduplicates, and probes each link with a HEAD request (falling back to GET when needed). Classifies links as ok, broken, redirect, timeout, or error. Reports internal vs. external link ratio.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Input: { "url": "https://example.com", "max_links": 100 }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### seo_keyword_density
|
|
80
|
+
|
|
81
|
+
Strips scripts, styles, and navigation, then tokenizes the remaining body text. Computes frequency and density for single words, bigrams, and trigrams (filtering stop words). When a target keyword is provided, checks whether it appears in the title, H1, meta description, and first paragraph.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Input: { "url": "https://example.com", "target_keyword": "seo tools" }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### seo_page_speed
|
|
88
|
+
|
|
89
|
+
Measures Time to First Byte and total download time from the server's perspective. Counts external scripts, stylesheets, images, and iframes. Detects render-blocking scripts (missing async/defer), images without alt text or dimensions, missing compression, and absence of lazy loading.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Input: { "url": "https://example.com" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### seo_sitemap_parse
|
|
96
|
+
|
|
97
|
+
Accepts a sitemap URL or any page URL (automatically appends `/sitemap.xml`). Falls back to checking `robots.txt` for a Sitemap directive. Handles both `<sitemapindex>` and `<urlset>` formats. Validates URL count limits, lastmod freshness, duplicate entries, protocol consistency, and trailing slash patterns.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Input: { "url": "https://example.com" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const meta_analyzer_js_1 = require("./tools/meta-analyzer.js");
|
|
8
|
+
const heading_checker_js_1 = require("./tools/heading-checker.js");
|
|
9
|
+
const link_checker_js_1 = require("./tools/link-checker.js");
|
|
10
|
+
const keyword_density_js_1 = require("./tools/keyword-density.js");
|
|
11
|
+
const page_speed_js_1 = require("./tools/page-speed.js");
|
|
12
|
+
const sitemap_parser_js_1 = require("./tools/sitemap-parser.js");
|
|
13
|
+
const server = new index_js_1.Server({
|
|
14
|
+
name: "mcp-seo-tools",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
}, {
|
|
17
|
+
capabilities: {
|
|
18
|
+
tools: {},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: "seo_meta_analyze",
|
|
25
|
+
description: "Analyze a page's meta tags, Open Graph tags, and Twitter Card tags. Returns an SEO score with actionable recommendations for title, description, social sharing tags, and other meta elements.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
url: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Full URL of the page to analyze (e.g. https://example.com)",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["url"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "seo_heading_check",
|
|
39
|
+
description: "Audit the heading hierarchy (H1-H6) of a page. Detects missing H1, skipped heading levels, duplicate headings, empty headings, and provides a visual hierarchy tree.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
url: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Full URL of the page to audit",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["url"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "seo_link_check",
|
|
53
|
+
description: "Crawl a page and check all links for broken URLs, redirects, and missing anchor text. Tests up to 50 links by default with concurrent requests.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
url: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Full URL of the page to scan for links",
|
|
60
|
+
},
|
|
61
|
+
max_links: {
|
|
62
|
+
type: "number",
|
|
63
|
+
description: "Maximum number of links to check (default: 50, max: 200)",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ["url"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "seo_keyword_density",
|
|
71
|
+
description: "Analyze keyword density and distribution across a page. Returns top single words, two-word and three-word phrases with density percentages. Optionally checks placement of a target keyword in title, H1, meta description, and first paragraph.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
url: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Full URL of the page to analyze",
|
|
78
|
+
},
|
|
79
|
+
target_keyword: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Optional target keyword to check placement and density for",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["url"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "seo_page_speed",
|
|
89
|
+
description: "Measure basic page speed metrics including Time to First Byte, total load time, HTML size, and resource counts (scripts, stylesheets, images). Identifies render-blocking resources and missing optimizations.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
url: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Full URL of the page to measure",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ["url"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "seo_sitemap_parse",
|
|
103
|
+
description: "Parse and analyze a website's sitemap.xml. Handles both sitemap indexes and URL sets. Reports URL counts, lastmod freshness, duplicate URLs, protocol consistency, and sitemap compliance issues. Automatically tries /sitemap.xml and falls back to robots.txt.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
url: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "URL of the sitemap or any page on the site (will try /sitemap.xml automatically)",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["url"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
}));
|
|
117
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
118
|
+
const { name, arguments: args } = request.params;
|
|
119
|
+
try {
|
|
120
|
+
switch (name) {
|
|
121
|
+
case "seo_meta_analyze": {
|
|
122
|
+
const result = await (0, meta_analyzer_js_1.analyzeMeta)(args?.url);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case "seo_heading_check": {
|
|
128
|
+
const result = await (0, heading_checker_js_1.checkHeadings)(args?.url);
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case "seo_link_check": {
|
|
134
|
+
const maxLinks = Math.min(args?.max_links || 50, 200);
|
|
135
|
+
const result = await (0, link_checker_js_1.checkLinks)(args?.url, maxLinks);
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "seo_keyword_density": {
|
|
141
|
+
const result = await (0, keyword_density_js_1.analyzeKeywordDensity)(args?.url, args?.target_keyword);
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
case "seo_page_speed": {
|
|
147
|
+
const result = await (0, page_speed_js_1.analyzePageSpeed)(args?.url);
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
case "seo_sitemap_parse": {
|
|
153
|
+
const result = await (0, sitemap_parser_js_1.parseSitemap)(args?.url);
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
async function main() {
|
|
170
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
171
|
+
await server.connect(transport);
|
|
172
|
+
console.error("MCP SEO Tools server running on stdio");
|
|
173
|
+
}
|
|
174
|
+
main().catch(console.error);
|
|
175
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface HeadingInfo {
|
|
2
|
+
level: number;
|
|
3
|
+
text: string;
|
|
4
|
+
id: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface HeadingAnalysis {
|
|
7
|
+
url: string;
|
|
8
|
+
headings: HeadingInfo[];
|
|
9
|
+
hierarchy: string;
|
|
10
|
+
counts: Record<string, number>;
|
|
11
|
+
issues: string[];
|
|
12
|
+
score: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function checkHeadings(url: string): Promise<HeadingAnalysis>;
|
|
15
|
+
//# sourceMappingURL=heading-checker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heading-checker.d.ts","sourceRoot":"","sources":["../../src/tools/heading-checker.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA2FzE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.checkHeadings = checkHeadings;
|
|
37
|
+
const cheerio = __importStar(require("cheerio"));
|
|
38
|
+
async function checkHeadings(url) {
|
|
39
|
+
const response = await fetch(url, {
|
|
40
|
+
headers: { "User-Agent": "MCPSEOTools/1.0 (SEO Analyzer)" },
|
|
41
|
+
redirect: "follow",
|
|
42
|
+
signal: AbortSignal.timeout(15000),
|
|
43
|
+
});
|
|
44
|
+
const html = await response.text();
|
|
45
|
+
const $ = cheerio.load(html);
|
|
46
|
+
const headings = [];
|
|
47
|
+
const counts = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 };
|
|
48
|
+
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
49
|
+
const tag = $(el).prop("tagName")?.toLowerCase() || "";
|
|
50
|
+
const level = parseInt(tag.replace("h", ""), 10);
|
|
51
|
+
const text = $(el).text().trim().replace(/\s+/g, " ");
|
|
52
|
+
const id = $(el).attr("id") || null;
|
|
53
|
+
headings.push({ level, text, id });
|
|
54
|
+
if (counts[tag] !== undefined)
|
|
55
|
+
counts[tag]++;
|
|
56
|
+
});
|
|
57
|
+
const issues = [];
|
|
58
|
+
// Check for missing H1
|
|
59
|
+
if (counts.h1 === 0) {
|
|
60
|
+
issues.push("No H1 tag found. Every page should have exactly one H1 as the main heading.");
|
|
61
|
+
}
|
|
62
|
+
else if (counts.h1 > 1) {
|
|
63
|
+
issues.push(`Found ${counts.h1} H1 tags. Use only one H1 per page for optimal SEO.`);
|
|
64
|
+
}
|
|
65
|
+
// Check if first heading is H1
|
|
66
|
+
if (headings.length > 0 && headings[0].level !== 1) {
|
|
67
|
+
issues.push(`First heading is H${headings[0].level} instead of H1. The first heading should be H1.`);
|
|
68
|
+
}
|
|
69
|
+
// Check for skipped levels
|
|
70
|
+
let previousLevel = 0;
|
|
71
|
+
for (const heading of headings) {
|
|
72
|
+
if (heading.level > previousLevel + 1 && previousLevel > 0) {
|
|
73
|
+
issues.push(`Heading level skipped: H${previousLevel} to H${heading.level} ("${heading.text.substring(0, 50)}"). Avoid skipping heading levels.`);
|
|
74
|
+
}
|
|
75
|
+
previousLevel = heading.level;
|
|
76
|
+
}
|
|
77
|
+
// Check for empty headings
|
|
78
|
+
const emptyHeadings = headings.filter((h) => h.text.length === 0);
|
|
79
|
+
if (emptyHeadings.length > 0) {
|
|
80
|
+
issues.push(`Found ${emptyHeadings.length} empty heading(s). All headings should contain descriptive text.`);
|
|
81
|
+
}
|
|
82
|
+
// Check for very long headings
|
|
83
|
+
const longHeadings = headings.filter((h) => h.text.length > 70);
|
|
84
|
+
for (const h of longHeadings) {
|
|
85
|
+
issues.push(`H${h.level} exceeds 70 characters (${h.text.length} chars): "${h.text.substring(0, 60)}..."`);
|
|
86
|
+
}
|
|
87
|
+
// Check for duplicate headings
|
|
88
|
+
const seen = new Map();
|
|
89
|
+
for (const h of headings) {
|
|
90
|
+
const key = `h${h.level}:${h.text.toLowerCase()}`;
|
|
91
|
+
seen.set(key, (seen.get(key) || 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
for (const [key, count] of seen) {
|
|
94
|
+
if (count > 1) {
|
|
95
|
+
const [tag, ...textParts] = key.split(":");
|
|
96
|
+
issues.push(`Duplicate ${tag.toUpperCase()}: "${textParts.join(":")}" appears ${count} times.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Build visual hierarchy
|
|
100
|
+
const hierarchy = headings
|
|
101
|
+
.map((h) => `${" ".repeat(h.level - 1)}H${h.level}: ${h.text.substring(0, 80)}`)
|
|
102
|
+
.join("\n");
|
|
103
|
+
// Score calculation
|
|
104
|
+
let score = 100;
|
|
105
|
+
if (counts.h1 === 0)
|
|
106
|
+
score -= 25;
|
|
107
|
+
else if (counts.h1 > 1)
|
|
108
|
+
score -= 15;
|
|
109
|
+
if (headings.length > 0 && headings[0].level !== 1)
|
|
110
|
+
score -= 10;
|
|
111
|
+
score -= Math.min(issues.filter((i) => i.includes("skipped")).length * 10, 20);
|
|
112
|
+
score -= emptyHeadings.length * 5;
|
|
113
|
+
score -= longHeadings.length * 3;
|
|
114
|
+
return {
|
|
115
|
+
url,
|
|
116
|
+
headings,
|
|
117
|
+
hierarchy,
|
|
118
|
+
counts,
|
|
119
|
+
issues,
|
|
120
|
+
score: Math.max(0, score),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=heading-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heading-checker.js","sourceRoot":"","sources":["../../src/tools/heading-checker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface KeywordInfo {
|
|
2
|
+
keyword: string;
|
|
3
|
+
count: number;
|
|
4
|
+
density: number;
|
|
5
|
+
inTitle: boolean;
|
|
6
|
+
inH1: boolean;
|
|
7
|
+
inHeadings: boolean;
|
|
8
|
+
inMetaDescription: boolean;
|
|
9
|
+
inFirstParagraph: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface KeywordDensityAnalysis {
|
|
12
|
+
url: string;
|
|
13
|
+
totalWords: number;
|
|
14
|
+
topSingleWords: KeywordInfo[];
|
|
15
|
+
topTwoWordPhrases: KeywordInfo[];
|
|
16
|
+
topThreeWordPhrases: KeywordInfo[];
|
|
17
|
+
targetKeywordAnalysis: KeywordInfo | null;
|
|
18
|
+
issues: string[];
|
|
19
|
+
recommendations: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function analyzeKeywordDensity(url: string, targetKeyword?: string): Promise<KeywordDensityAnalysis>;
|
|
22
|
+
//# sourceMappingURL=keyword-density.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyword-density.d.ts","sourceRoot":"","sources":["../../src/tools/keyword-density.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,WAAW,EAAE,CAAC;IAC9B,iBAAiB,EAAE,WAAW,EAAE,CAAC;IACjC,mBAAmB,EAAE,WAAW,EAAE,CAAC;IACnC,qBAAqB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AA6DD,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAgGhH"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeKeywordDensity = analyzeKeywordDensity;
|
|
37
|
+
const cheerio = __importStar(require("cheerio"));
|
|
38
|
+
const STOP_WORDS = new Set([
|
|
39
|
+
"a", "an", "the", "and", "or", "but", "is", "are", "was", "were", "be", "been",
|
|
40
|
+
"being", "have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
41
|
+
"should", "may", "might", "shall", "can", "need", "dare", "ought", "used",
|
|
42
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from", "as", "into",
|
|
43
|
+
"through", "during", "before", "after", "above", "below", "between", "out",
|
|
44
|
+
"off", "over", "under", "again", "further", "then", "once", "here", "there",
|
|
45
|
+
"when", "where", "why", "how", "all", "each", "every", "both", "few", "more",
|
|
46
|
+
"most", "other", "some", "such", "no", "nor", "not", "only", "own", "same",
|
|
47
|
+
"so", "than", "too", "very", "just", "because", "about", "up", "its", "it",
|
|
48
|
+
"this", "that", "these", "those", "i", "me", "my", "we", "our", "you", "your",
|
|
49
|
+
"he", "him", "his", "she", "her", "they", "them", "their", "what", "which",
|
|
50
|
+
"who", "whom", "if", "also", "get", "got", "like", "make", "go", "new",
|
|
51
|
+
]);
|
|
52
|
+
function extractVisibleText($) {
|
|
53
|
+
$("script, style, noscript, nav, footer, header").remove();
|
|
54
|
+
return $("body").text().replace(/\s+/g, " ").trim().toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
function tokenize(text) {
|
|
57
|
+
return text.match(/[a-z]{2,}/g) || [];
|
|
58
|
+
}
|
|
59
|
+
function getNgrams(words, n) {
|
|
60
|
+
const counts = new Map();
|
|
61
|
+
for (let i = 0; i <= words.length - n; i++) {
|
|
62
|
+
const chunk = words.slice(i, i + n);
|
|
63
|
+
// Skip ngrams that start or end with stop words
|
|
64
|
+
if (STOP_WORDS.has(chunk[0]) || STOP_WORDS.has(chunk[chunk.length - 1]))
|
|
65
|
+
continue;
|
|
66
|
+
const phrase = chunk.join(" ");
|
|
67
|
+
counts.set(phrase, (counts.get(phrase) || 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
return counts;
|
|
70
|
+
}
|
|
71
|
+
function buildKeywordInfo(keyword, count, totalWords, title, h1Text, headingsText, metaDesc, firstParagraph) {
|
|
72
|
+
const kw = keyword.toLowerCase();
|
|
73
|
+
return {
|
|
74
|
+
keyword,
|
|
75
|
+
count,
|
|
76
|
+
density: parseFloat(((count / totalWords) * 100).toFixed(2)),
|
|
77
|
+
inTitle: title.includes(kw),
|
|
78
|
+
inH1: h1Text.includes(kw),
|
|
79
|
+
inHeadings: headingsText.includes(kw),
|
|
80
|
+
inMetaDescription: metaDesc.includes(kw),
|
|
81
|
+
inFirstParagraph: firstParagraph.includes(kw),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function analyzeKeywordDensity(url, targetKeyword) {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
headers: { "User-Agent": "MCPSEOTools/1.0 (SEO Analyzer)" },
|
|
87
|
+
redirect: "follow",
|
|
88
|
+
signal: AbortSignal.timeout(15000),
|
|
89
|
+
});
|
|
90
|
+
const html = await response.text();
|
|
91
|
+
const $ = cheerio.load(html);
|
|
92
|
+
const title = $("title").first().text().trim().toLowerCase();
|
|
93
|
+
const metaDesc = ($('meta[name="description"]').attr("content") || "").toLowerCase();
|
|
94
|
+
const h1Text = $("h1").text().trim().toLowerCase();
|
|
95
|
+
const headingsText = $("h1, h2, h3, h4, h5, h6").text().trim().toLowerCase();
|
|
96
|
+
const firstParagraph = $("p").first().text().trim().toLowerCase();
|
|
97
|
+
const bodyText = extractVisibleText($);
|
|
98
|
+
const allWords = tokenize(bodyText);
|
|
99
|
+
const contentWords = allWords.filter((w) => !STOP_WORDS.has(w));
|
|
100
|
+
const totalWords = allWords.length;
|
|
101
|
+
// Single words
|
|
102
|
+
const singleCounts = new Map();
|
|
103
|
+
for (const w of contentWords) {
|
|
104
|
+
singleCounts.set(w, (singleCounts.get(w) || 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
const topSingle = [...singleCounts.entries()]
|
|
107
|
+
.filter(([, c]) => c >= 2)
|
|
108
|
+
.sort((a, b) => b[1] - a[1])
|
|
109
|
+
.slice(0, 15)
|
|
110
|
+
.map(([kw, count]) => buildKeywordInfo(kw, count, totalWords, title, h1Text, headingsText, metaDesc, firstParagraph));
|
|
111
|
+
// Two-word phrases
|
|
112
|
+
const twoCounts = getNgrams(allWords, 2);
|
|
113
|
+
const topTwo = [...twoCounts.entries()]
|
|
114
|
+
.filter(([, c]) => c >= 2)
|
|
115
|
+
.sort((a, b) => b[1] - a[1])
|
|
116
|
+
.slice(0, 10)
|
|
117
|
+
.map(([kw, count]) => buildKeywordInfo(kw, count, totalWords, title, h1Text, headingsText, metaDesc, firstParagraph));
|
|
118
|
+
// Three-word phrases
|
|
119
|
+
const threeCounts = getNgrams(allWords, 3);
|
|
120
|
+
const topThree = [...threeCounts.entries()]
|
|
121
|
+
.filter(([, c]) => c >= 2)
|
|
122
|
+
.sort((a, b) => b[1] - a[1])
|
|
123
|
+
.slice(0, 10)
|
|
124
|
+
.map(([kw, count]) => buildKeywordInfo(kw, count, totalWords, title, h1Text, headingsText, metaDesc, firstParagraph));
|
|
125
|
+
// Target keyword analysis
|
|
126
|
+
let targetKeywordAnalysis = null;
|
|
127
|
+
if (targetKeyword) {
|
|
128
|
+
const tk = targetKeyword.toLowerCase().trim();
|
|
129
|
+
const regex = new RegExp(`\\b${tk.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
|
|
130
|
+
const matches = bodyText.match(regex);
|
|
131
|
+
const count = matches ? matches.length : 0;
|
|
132
|
+
targetKeywordAnalysis = buildKeywordInfo(tk, count, totalWords, title, h1Text, headingsText, metaDesc, firstParagraph);
|
|
133
|
+
}
|
|
134
|
+
const issues = [];
|
|
135
|
+
const recommendations = [];
|
|
136
|
+
if (totalWords < 300) {
|
|
137
|
+
issues.push(`Page has only ${totalWords} words. Search engines prefer pages with at least 300 words.`);
|
|
138
|
+
recommendations.push("Add more substantial content to improve topical authority.");
|
|
139
|
+
}
|
|
140
|
+
if (targetKeywordAnalysis) {
|
|
141
|
+
const tk = targetKeywordAnalysis;
|
|
142
|
+
if (tk.count === 0) {
|
|
143
|
+
issues.push(`Target keyword "${tk.keyword}" not found in page content.`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (tk.density < 0.5)
|
|
147
|
+
recommendations.push(`Target keyword density is low (${tk.density}%). Aim for 1-2%.`);
|
|
148
|
+
if (tk.density > 3)
|
|
149
|
+
issues.push(`Target keyword density is too high (${tk.density}%). This may be seen as keyword stuffing.`);
|
|
150
|
+
if (!tk.inTitle)
|
|
151
|
+
recommendations.push("Include the target keyword in the page title.");
|
|
152
|
+
if (!tk.inH1)
|
|
153
|
+
recommendations.push("Include the target keyword in the H1 heading.");
|
|
154
|
+
if (!tk.inMetaDescription)
|
|
155
|
+
recommendations.push("Include the target keyword in the meta description.");
|
|
156
|
+
if (!tk.inFirstParagraph)
|
|
157
|
+
recommendations.push("Mention the target keyword within the first paragraph.");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Check for potential keyword stuffing in top words
|
|
161
|
+
const stuffed = topSingle.filter((k) => k.density > 4);
|
|
162
|
+
for (const k of stuffed) {
|
|
163
|
+
issues.push(`"${k.keyword}" appears ${k.count} times (${k.density}% density). This may trigger keyword stuffing penalties.`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
url,
|
|
167
|
+
totalWords,
|
|
168
|
+
topSingleWords: topSingle,
|
|
169
|
+
topTwoWordPhrases: topTwo,
|
|
170
|
+
topThreeWordPhrases: topThree,
|
|
171
|
+
targetKeywordAnalysis,
|
|
172
|
+
issues,
|
|
173
|
+
recommendations,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=keyword-density.js.map
|