seo-gravity-mcp 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 +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +521 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +76 -0
- package/dist/tools/eeat.d.ts +3 -0
- package/dist/tools/eeat.js +155 -0
- package/dist/tools/geo.d.ts +17 -0
- package/dist/tools/geo.js +190 -0
- package/dist/tools/keywords.d.ts +26 -0
- package/dist/tools/keywords.js +112 -0
- package/dist/tools/onpage.d.ts +15 -0
- package/dist/tools/onpage.js +254 -0
- package/dist/tools/performance.d.ts +32 -0
- package/dist/tools/performance.js +142 -0
- package/dist/tools/schema.d.ts +8 -0
- package/dist/tools/schema.js +191 -0
- package/dist/tools/serp.d.ts +6 -0
- package/dist/tools/serp.js +225 -0
- package/dist/tools/technical.d.ts +36 -0
- package/dist/tools/technical.js +277 -0
- package/dist/types/seo.d.ts +354 -0
- package/dist/types/seo.js +1 -0
- package/dist/utils/jsdomRenderer.d.ts +5 -0
- package/dist/utils/jsdomRenderer.js +79 -0
- package/dist/utils/nlp.d.ts +41 -0
- package/dist/utils/nlp.js +214 -0
- package/dist/utils/scraper.d.ts +36 -0
- package/dist/utils/scraper.js +129 -0
- package/dist/utils/searchEngines.d.ts +17 -0
- package/dist/utils/searchEngines.js +197 -0
- package/package.json +61 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { fetchAndParsePage } from '../utils/scraper.js';
|
|
2
|
+
import { scrapeGoogleSerp } from '../utils/searchEngines.js';
|
|
3
|
+
import { extractEntitiesWithSalience } from '../utils/nlp.js';
|
|
4
|
+
export async function scoreInformationGain(myContentOrUrl, targetKeyword) {
|
|
5
|
+
let myText = myContentOrUrl;
|
|
6
|
+
if (myContentOrUrl.startsWith('http') || myContentOrUrl.includes('<html') || myContentOrUrl.endsWith('.html')) {
|
|
7
|
+
const page = await fetchAndParsePage(myContentOrUrl);
|
|
8
|
+
myText = page.cleanText;
|
|
9
|
+
}
|
|
10
|
+
// 1. Fetch top competitor texts
|
|
11
|
+
const serp = await scrapeGoogleSerp(targetKeyword, 'us', 'en', 3);
|
|
12
|
+
const competitorTexts = [];
|
|
13
|
+
for (const r of serp.organicResults.slice(0, 3)) {
|
|
14
|
+
try {
|
|
15
|
+
const p = await fetchAndParsePage(r.url);
|
|
16
|
+
competitorTexts.push(p.cleanText);
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
}
|
|
20
|
+
const myEntities = extractEntitiesWithSalience(myText);
|
|
21
|
+
const compEntities = extractEntitiesWithSalience(competitorTexts.join(' '));
|
|
22
|
+
const compEntityNames = new Set(compEntities.map(e => e.name.toLowerCase()));
|
|
23
|
+
const uniqueEntities = myEntities
|
|
24
|
+
.filter(e => !compEntityNames.has(e.name.toLowerCase()))
|
|
25
|
+
.map(e => e.name);
|
|
26
|
+
// Detect specific novel signals: data points, unique methodologies, case studies
|
|
27
|
+
const dataPointsAndStats = (myText.match(/\b\d+(\.\d+)?%\b|\b\$\d+(\.\d+)?\b|\b\d{2,}\s+(users|companies|participants|queries|customers|nodes)\b/gi) || [])
|
|
28
|
+
.slice(0, 6);
|
|
29
|
+
const caseStudiesOrExamples = (myText.match(/\b(for example|case study|in our testing|we found that|our data shows|in practice)\b[\s\S]{10,80}\./gi) || [])
|
|
30
|
+
.slice(0, 4);
|
|
31
|
+
const uniqueQuotes = (myText.match(/"([^"]{15,100})"/g) || []).slice(0, 3);
|
|
32
|
+
// Score calculation:
|
|
33
|
+
// Base 30
|
|
34
|
+
// + 5 per unique entity (max 25)
|
|
35
|
+
// + 5 per distinct stat (max 20)
|
|
36
|
+
// + 10 per case study/test result (max 20)
|
|
37
|
+
// + 5 per quote (max 10)
|
|
38
|
+
let rawScore = 30 + (uniqueEntities.length * 5) + (dataPointsAndStats.length * 4) + (caseStudiesOrExamples.length * 7) + (uniqueQuotes.length * 3);
|
|
39
|
+
const informationGainScore = Math.min(100, Math.max(10, rawScore));
|
|
40
|
+
const noveltyTier = informationGainScore >= 75
|
|
41
|
+
? 'Exceptional (High Information Gain)'
|
|
42
|
+
: informationGainScore >= 50
|
|
43
|
+
? 'Moderate'
|
|
44
|
+
: 'Low (Rehashed / Generic AI Risk)';
|
|
45
|
+
const recommendations = [];
|
|
46
|
+
if (dataPointsAndStats.length < 2) {
|
|
47
|
+
recommendations.push('Include proprietary metrics, benchmark percentages, or original experiment numbers to distinguish from generic summary articles.');
|
|
48
|
+
}
|
|
49
|
+
if (caseStudiesOrExamples.length === 0) {
|
|
50
|
+
recommendations.push('Add a real-world case study or first-person testing walkthrough ("In our tests...", "When we implemented X...").');
|
|
51
|
+
}
|
|
52
|
+
if (uniqueEntities.length < 3) {
|
|
53
|
+
recommendations.push('Introduce unique frameworks, tools, or named methodologies not already saturated across top competitor pages.');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
targetKeyword,
|
|
57
|
+
informationGainScore,
|
|
58
|
+
noveltyTier,
|
|
59
|
+
uniqueEntitiesDetected: uniqueEntities.slice(0, 8),
|
|
60
|
+
competitorOverlapPercentage: Math.max(20, 100 - uniqueEntities.length * 8),
|
|
61
|
+
originalElementsFound: {
|
|
62
|
+
dataPointsAndStats,
|
|
63
|
+
caseStudiesOrExamples,
|
|
64
|
+
uniqueMethodologiesOrQuotes: uniqueQuotes
|
|
65
|
+
},
|
|
66
|
+
recommendationsToIncreaseGain: recommendations
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function auditEeat(urlOrHtml) {
|
|
70
|
+
const page = await fetchAndParsePage(urlOrHtml);
|
|
71
|
+
const html = page.html;
|
|
72
|
+
const cleanText = page.cleanText;
|
|
73
|
+
// 1. Author signals
|
|
74
|
+
const hasAuthorByline = /\b(by\s+[A-Z][a-z]+|written by|author|editorial team)\b/i.test(html) || page.$('[rel="author"], .author, .byline, [itemprop="author"]').length > 0;
|
|
75
|
+
const hasAuthorBio = /\b(bio|about the author|author-bio|experience|credentials)\b/i.test(html);
|
|
76
|
+
const personSchemas = page.schemas.filter(s => s['@type'] === 'Person' || (s['@graph'] && s['@graph'].some((g) => g['@type'] === 'Person')));
|
|
77
|
+
const hasPersonSchema = personSchemas.length > 0;
|
|
78
|
+
const sameAsProfiles = [];
|
|
79
|
+
page.$('a[href*="linkedin.com"], a[href*="twitter.com"], a[href*="x.com"], a[href*="wikipedia.org"], a[href*="wikidata.org"]').each((_, el) => {
|
|
80
|
+
const href = page.$(el).attr('href');
|
|
81
|
+
if (href && !sameAsProfiles.includes(href)) {
|
|
82
|
+
sameAsProfiles.push(href);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// 2. Transparency signals
|
|
86
|
+
const hasEditorialPolicy = /editorial\s+policy|fact\s+check|correction\s+policy|methodology/i.test(html);
|
|
87
|
+
const hasFactCheckDisclaimer = /fact-checked|medical\s+review|financial\s+disclaimer|reviewed\s+by/i.test(html);
|
|
88
|
+
const hasPublishDate = /datePublished|published\s+on|date-published|<time/i.test(html);
|
|
89
|
+
const hasModifiedDate = /dateModified|updated\s+on|last\s+updated|last-modified/i.test(html);
|
|
90
|
+
// 3. Contact & Entity signals
|
|
91
|
+
const hasAboutPageLink = page.links.internal.some(l => /about|company|team/i.test(l)) || page.links.external.some(l => /about/i.test(l));
|
|
92
|
+
const hasContactInfo = /contact|support|mailto:|tel:|customer-service/i.test(html);
|
|
93
|
+
const hasPhysicalAddress = /address|postalCode|streetAddress|HQ|headquarters/i.test(html);
|
|
94
|
+
// 4. External authoritative citations
|
|
95
|
+
const govEduOrgLinks = page.links.external.filter(l => /\.gov|\.edu|\.org|wikipedia\.org|ncbi\.nlm\.nih\.gov/i.test(l)).length;
|
|
96
|
+
// Calculate score
|
|
97
|
+
let score = 20;
|
|
98
|
+
if (hasAuthorByline)
|
|
99
|
+
score += 15;
|
|
100
|
+
if (hasAuthorBio)
|
|
101
|
+
score += 10;
|
|
102
|
+
if (hasPersonSchema)
|
|
103
|
+
score += 15;
|
|
104
|
+
if (sameAsProfiles.length > 0)
|
|
105
|
+
score += 10;
|
|
106
|
+
if (hasEditorialPolicy || hasFactCheckDisclaimer)
|
|
107
|
+
score += 10;
|
|
108
|
+
if (hasPublishDate && hasModifiedDate)
|
|
109
|
+
score += 10;
|
|
110
|
+
if (hasAboutPageLink && hasContactInfo)
|
|
111
|
+
score += 10;
|
|
112
|
+
const overallEeatScore = Math.min(100, score);
|
|
113
|
+
const trustLevel = overallEeatScore >= 75 ? 'High Authority' : overallEeatScore >= 50 ? 'Moderate' : 'Needs Improvement';
|
|
114
|
+
const improvements = [];
|
|
115
|
+
if (!hasPersonSchema) {
|
|
116
|
+
improvements.push('Embed Schema.org Person structured data for the author with explicit sameAs links (LinkedIn, Wikidata).');
|
|
117
|
+
}
|
|
118
|
+
if (!hasAuthorBio) {
|
|
119
|
+
improvements.push('Add an author biography highlighting subject-matter credentials, years of experience, and industry background.');
|
|
120
|
+
}
|
|
121
|
+
if (!hasModifiedDate) {
|
|
122
|
+
improvements.push('Display visible "Last Updated / Modified" timestamps to reinforce freshness.');
|
|
123
|
+
}
|
|
124
|
+
if (sameAsProfiles.length === 0) {
|
|
125
|
+
improvements.push('Link author social profiles (LinkedIn, X/Twitter, personal website) to establish clear verifiable entity connection.');
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
overallEeatScore,
|
|
129
|
+
trustLevel,
|
|
130
|
+
signals: {
|
|
131
|
+
authorIdentity: {
|
|
132
|
+
hasAuthorByline,
|
|
133
|
+
hasAuthorBio,
|
|
134
|
+
hasPersonSchema,
|
|
135
|
+
sameAsProfilesLinked: sameAsProfiles.slice(0, 5)
|
|
136
|
+
},
|
|
137
|
+
transparency: {
|
|
138
|
+
hasEditorialPolicy,
|
|
139
|
+
hasFactCheckDisclaimer,
|
|
140
|
+
hasPublishDate,
|
|
141
|
+
hasModifiedDate
|
|
142
|
+
},
|
|
143
|
+
contactAndEntity: {
|
|
144
|
+
hasAboutPageLink,
|
|
145
|
+
hasContactInfo,
|
|
146
|
+
hasPhysicalAddress
|
|
147
|
+
},
|
|
148
|
+
citationsAndReferences: {
|
|
149
|
+
externalAuthoritativeCitationsCount: page.links.external.length,
|
|
150
|
+
peerReviewedOrGovLinksCount: govEduOrgLinks
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
actionableImprovements: improvements
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GeoAiReadinessReport } from '../types/seo.js';
|
|
2
|
+
export declare function auditGeoAiReadiness(urlOrText: string, targetQuery: string): Promise<GeoAiReadinessReport>;
|
|
3
|
+
export declare function generateLlmsTxt(siteName: string, siteDescription: string, keyPages: Array<{
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}>): {
|
|
8
|
+
llmsTxt: string;
|
|
9
|
+
llmsFullTxt: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function auditAiBotsRobots(domainOrUrl: string): Promise<{
|
|
12
|
+
domain: string;
|
|
13
|
+
robotsTxtFound: boolean;
|
|
14
|
+
botDirectives: Record<string, 'Allowed' | 'Disallowed' | 'Default (Allowed)'>;
|
|
15
|
+
summary: string;
|
|
16
|
+
recommendedConfig: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { fetchAndParsePage, getRandomUserAgent } from '../utils/scraper.js';
|
|
3
|
+
export async function auditGeoAiReadiness(urlOrText, targetQuery) {
|
|
4
|
+
let content = urlOrText;
|
|
5
|
+
let hasHeadings = false;
|
|
6
|
+
let listItemsCount = 0;
|
|
7
|
+
let tablesCount = 0;
|
|
8
|
+
if (urlOrText.startsWith('http') || urlOrText.includes('<html') || urlOrText.endsWith('.html')) {
|
|
9
|
+
const page = await fetchAndParsePage(urlOrText);
|
|
10
|
+
content = page.cleanText;
|
|
11
|
+
hasHeadings = page.headings.h2.length > 0;
|
|
12
|
+
listItemsCount = page.$('li').length;
|
|
13
|
+
tablesCount = page.$('table').length;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
hasHeadings = /^#{1,4}\s+/m.test(urlOrText);
|
|
17
|
+
listItemsCount = (urlOrText.match(/^[-*]\s+/gm) || []).length;
|
|
18
|
+
tablesCount = (urlOrText.match(/\|[\s\S]*?\|/g) || []).length > 2 ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
const queryWords = targetQuery.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
21
|
+
const first300Words = content.split(/\s+/).slice(0, 300).join(' ').toLowerCase();
|
|
22
|
+
// 1. Direct Answer Check: Does the first 150 words contain a direct definition or concise answer?
|
|
23
|
+
const directAnswerPassed = queryWords.some(w => first300Words.includes(w)) &&
|
|
24
|
+
(first300Words.includes(' is ') || first300Words.includes(' refers to ') || first300Words.includes(' can be defined as '));
|
|
25
|
+
const directAnswerScore = directAnswerPassed ? 25 : 10;
|
|
26
|
+
// 2. Semantic Chunking Check: Are there distinct sections with clear H2/H3 anchors?
|
|
27
|
+
const semanticChunkingPassed = hasHeadings && content.length > 500;
|
|
28
|
+
const chunkingScore = semanticChunkingPassed ? 20 : 5;
|
|
29
|
+
// 3. Structured Data & Lists: Are there bullet points or comparison tables for LLM digestion?
|
|
30
|
+
const structuredDataPassed = listItemsCount >= 3 || tablesCount >= 1;
|
|
31
|
+
const structuredScore = structuredDataPassed ? 20 : 5;
|
|
32
|
+
// 4. Authoritative Citations & Stats: Are there numbers, %, or citations?
|
|
33
|
+
const statsMatches = content.match(/\b\d+(\.\d+)?%\b|\b\$\d+(\.\d+)?\b|\b\d{4}\b|\baccording to\b|\bstudy by\b/gi) || [];
|
|
34
|
+
const statsScore = statsMatches.length >= 3 ? 20 : statsMatches.length >= 1 ? 10 : 0;
|
|
35
|
+
// 5. Entity Clarity
|
|
36
|
+
const entityScore = queryWords.every(w => content.toLowerCase().includes(w)) ? 15 : 5;
|
|
37
|
+
const totalScore = directAnswerScore + chunkingScore + structuredScore + statsScore + entityScore;
|
|
38
|
+
const citationLikelihood = totalScore >= 75 ? 'High' : totalScore >= 50 ? 'Medium' : 'Low';
|
|
39
|
+
const firstSentence = content.split(/[.!?]+/)[0] || '';
|
|
40
|
+
return {
|
|
41
|
+
targetQuery,
|
|
42
|
+
overallGeoScore: totalScore,
|
|
43
|
+
citationLikelihood,
|
|
44
|
+
checks: {
|
|
45
|
+
directAnswerParagraph: {
|
|
46
|
+
passed: directAnswerPassed,
|
|
47
|
+
score: directAnswerScore,
|
|
48
|
+
feedback: directAnswerPassed
|
|
49
|
+
? 'Clear introductory direct answer detected, ideal for AI Overview snippet extraction.'
|
|
50
|
+
: 'Missing a bold, concise definition or executive summary in the first 100-150 words.',
|
|
51
|
+
detectedSnippet: firstSentence.substring(0, 150)
|
|
52
|
+
},
|
|
53
|
+
semanticChunking: {
|
|
54
|
+
passed: semanticChunkingPassed,
|
|
55
|
+
score: chunkingScore,
|
|
56
|
+
feedback: semanticChunkingPassed
|
|
57
|
+
? 'Content is partitioned into distinct semantic subtopics with heading markers.'
|
|
58
|
+
: 'Content lacks clear modular headings, reducing LLM citation accuracy.'
|
|
59
|
+
},
|
|
60
|
+
structuredDataAndLists: {
|
|
61
|
+
passed: structuredDataPassed,
|
|
62
|
+
score: structuredScore,
|
|
63
|
+
feedback: `Found ${listItemsCount} bullet items and ${tablesCount} tables. LLMs favor bulleted lists and tables when generating comparison answers.`
|
|
64
|
+
},
|
|
65
|
+
authoritativeCitationsAndStats: {
|
|
66
|
+
passed: statsMatches.length >= 2,
|
|
67
|
+
score: statsScore,
|
|
68
|
+
feedback: `Detected ${statsMatches.length} specific data points/statistics. Hard data significantly increases AI citation probability.`,
|
|
69
|
+
detectedStatsCount: statsMatches.length
|
|
70
|
+
},
|
|
71
|
+
entityClarity: {
|
|
72
|
+
passed: entityScore === 15,
|
|
73
|
+
score: entityScore,
|
|
74
|
+
feedback: 'Primary subject entity is clearly referenced throughout the document.'
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
recommendedSnippetsForAiCitation: [
|
|
78
|
+
{
|
|
79
|
+
section: 'Executive Summary / Direct Answer Box',
|
|
80
|
+
suggestedFormat: '3-sentence summary in bold or callout box',
|
|
81
|
+
exampleText: `**${targetQuery}** refers to [concise definition]. It allows [key benefit] and is primarily used for [core use case].`
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
section: 'Key Takeaways Bullet Points',
|
|
85
|
+
suggestedFormat: 'Unordered list of 3-5 high-impact bullet items',
|
|
86
|
+
exampleText: `• Core Feature 1: [Specific metric or capability]\n• Core Feature 2: [Specific metric or capability]\n• Key Difference: [How it differs from alternatives]`
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function generateLlmsTxt(siteName, siteDescription, keyPages) {
|
|
92
|
+
const llmsTxt = `# ${siteName}
|
|
93
|
+
|
|
94
|
+
> ${siteDescription}
|
|
95
|
+
|
|
96
|
+
## Core Documentation & Key Pages
|
|
97
|
+
${keyPages.map(p => `- [${p.title}](${p.url}): ${p.description}`).join('\n')}
|
|
98
|
+
|
|
99
|
+
## Guidelines for AI Ingestion
|
|
100
|
+
- Prefer concise markdown extraction.
|
|
101
|
+
- Link citations directly to the canonical URLs listed above.
|
|
102
|
+
`;
|
|
103
|
+
const llmsFullTxt = `# ${siteName} - Comprehensive AI Knowledge Index
|
|
104
|
+
|
|
105
|
+
> ${siteDescription}
|
|
106
|
+
|
|
107
|
+
## Table of Contents
|
|
108
|
+
${keyPages.map(p => `- [${p.title}](#${p.title.toLowerCase().replace(/\s+/g, '-')})`).join('\n')}
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
${keyPages.map(p => `### ${p.title}
|
|
113
|
+
- **URL**: ${p.url}
|
|
114
|
+
- **Description**: ${p.description}
|
|
115
|
+
- **Canonical Reference**: Direct citation recommended for queries regarding ${p.title.toLowerCase()}.
|
|
116
|
+
`).join('\n---\n')}
|
|
117
|
+
`;
|
|
118
|
+
return { llmsTxt, llmsFullTxt };
|
|
119
|
+
}
|
|
120
|
+
export async function auditAiBotsRobots(domainOrUrl) {
|
|
121
|
+
const domain = domainOrUrl.startsWith('http') ? new URL(domainOrUrl).origin : `https://${domainOrUrl}`;
|
|
122
|
+
const robotsUrl = `${domain}/robots.txt`;
|
|
123
|
+
let robotsContent = '';
|
|
124
|
+
let found = false;
|
|
125
|
+
try {
|
|
126
|
+
const res = await axios.get(robotsUrl, {
|
|
127
|
+
headers: { 'User-Agent': getRandomUserAgent() },
|
|
128
|
+
timeout: 8000,
|
|
129
|
+
validateStatus: () => true
|
|
130
|
+
});
|
|
131
|
+
if (res.status === 200 && typeof res.data === 'string') {
|
|
132
|
+
robotsContent = res.data;
|
|
133
|
+
found = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
found = false;
|
|
138
|
+
}
|
|
139
|
+
const aiBots = [
|
|
140
|
+
'GPTBot',
|
|
141
|
+
'ChatGPT-User',
|
|
142
|
+
'ClaudeBot',
|
|
143
|
+
'anthropic-ai',
|
|
144
|
+
'PerplexityBot',
|
|
145
|
+
'Google-Extended',
|
|
146
|
+
'Bytespider',
|
|
147
|
+
'Applebot-Extended',
|
|
148
|
+
'CCBot'
|
|
149
|
+
];
|
|
150
|
+
const botDirectives = {};
|
|
151
|
+
aiBots.forEach(bot => {
|
|
152
|
+
const regex = new RegExp(`User-agent:\\s*${bot}[\\s\\S]*?Disallow:\\s*(\\/|.*)`, 'i');
|
|
153
|
+
if (regex.test(robotsContent)) {
|
|
154
|
+
const match = robotsContent.match(regex);
|
|
155
|
+
if (match && match[1] && match[1].trim() === '/') {
|
|
156
|
+
botDirectives[bot] = 'Disallowed';
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
botDirectives[bot] = 'Allowed';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Check if global User-agent: * disallows all
|
|
164
|
+
const globalDisallow = /User-agent:\s*\*[\s\S]*?Disallow:\s*\/\s*$/m.test(robotsContent);
|
|
165
|
+
botDirectives[bot] = globalDisallow ? 'Disallowed' : 'Default (Allowed)';
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const disallowedCount = Object.values(botDirectives).filter(v => v === 'Disallowed').length;
|
|
169
|
+
return {
|
|
170
|
+
domain,
|
|
171
|
+
robotsTxtFound: found,
|
|
172
|
+
botDirectives,
|
|
173
|
+
summary: disallowedCount > 0
|
|
174
|
+
? `${disallowedCount} AI crawlers are explicitly disallowed from indexing content.`
|
|
175
|
+
: 'All major AI search crawlers (GPTBot, ClaudeBot, PerplexityBot) are currently allowed.',
|
|
176
|
+
recommendedConfig: `# Recommended AI Bot Directives in robots.txt
|
|
177
|
+
User-agent: GPTBot
|
|
178
|
+
Allow: /
|
|
179
|
+
|
|
180
|
+
User-agent: ClaudeBot
|
|
181
|
+
Allow: /
|
|
182
|
+
|
|
183
|
+
User-agent: PerplexityBot
|
|
184
|
+
Allow: /
|
|
185
|
+
|
|
186
|
+
User-agent: Google-Extended
|
|
187
|
+
Allow: /
|
|
188
|
+
`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { KeywordClusterGroup, SearchIntentClassification } from '../types/seo.js';
|
|
2
|
+
export declare function getKeywordSuggestions(seedKeyword: string, includeAlphabetSoup?: boolean, modifiers?: string[]): Promise<{
|
|
3
|
+
seed: string;
|
|
4
|
+
totalSuggestions: number;
|
|
5
|
+
coreSuggestions: string[];
|
|
6
|
+
modifierSuggestions: Record<string, string[]>;
|
|
7
|
+
alphabetSoupSuggestions?: Record<string, string[]>;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function findQuestions(topic: string): Promise<{
|
|
10
|
+
topic: string;
|
|
11
|
+
totalQuestions: number;
|
|
12
|
+
questionClusters: {
|
|
13
|
+
how: string[];
|
|
14
|
+
what: string[];
|
|
15
|
+
why: string[];
|
|
16
|
+
can: string[];
|
|
17
|
+
is: string[];
|
|
18
|
+
best: string[];
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
export declare function clusterKeywords(keywords: string[], similarityThreshold?: number): {
|
|
22
|
+
totalKeywords: number;
|
|
23
|
+
clusterCount: number;
|
|
24
|
+
clusters: KeywordClusterGroup[];
|
|
25
|
+
};
|
|
26
|
+
export declare function classifySearchIntent(keywords: string[]): SearchIntentClassification[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getGoogleAutocomplete, getAlphabetSoupSuggestions } from '../utils/searchEngines.js';
|
|
2
|
+
export async function getKeywordSuggestions(seedKeyword, includeAlphabetSoup = true, modifiers = ['best', 'vs', 'pricing', 'alternative', 'how to', 'free']) {
|
|
3
|
+
const core = await getGoogleAutocomplete(seedKeyword);
|
|
4
|
+
const modResults = {};
|
|
5
|
+
await Promise.all(modifiers.map(async (mod) => {
|
|
6
|
+
const results = await getGoogleAutocomplete(`${seedKeyword} ${mod}`);
|
|
7
|
+
if (results.length > 0)
|
|
8
|
+
modResults[mod] = results.slice(0, 5);
|
|
9
|
+
}));
|
|
10
|
+
let alphabetSoup;
|
|
11
|
+
if (includeAlphabetSoup) {
|
|
12
|
+
alphabetSoup = await getAlphabetSoupSuggestions(seedKeyword);
|
|
13
|
+
}
|
|
14
|
+
let total = core.length;
|
|
15
|
+
Object.values(modResults).forEach(arr => total += arr.length);
|
|
16
|
+
if (alphabetSoup) {
|
|
17
|
+
Object.values(alphabetSoup).forEach(arr => total += arr.length);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
seed: seedKeyword,
|
|
21
|
+
totalSuggestions: total,
|
|
22
|
+
coreSuggestions: core,
|
|
23
|
+
modifierSuggestions: modResults,
|
|
24
|
+
alphabetSoupSuggestions: alphabetSoup
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function findQuestions(topic) {
|
|
28
|
+
const questionWords = ['how to', 'what is', 'why does', 'can you', 'is it', 'best'];
|
|
29
|
+
const clusters = { how: [], what: [], why: [], can: [], is: [], best: [] };
|
|
30
|
+
await Promise.all(questionWords.map(async (q) => {
|
|
31
|
+
const results = await getGoogleAutocomplete(`${q} ${topic}`);
|
|
32
|
+
const key = q.split(' ')[0];
|
|
33
|
+
if (clusters[key]) {
|
|
34
|
+
clusters[key] = results.filter(r => r.length > 10).slice(0, 6);
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
let total = 0;
|
|
38
|
+
Object.values(clusters).forEach((arr) => total += arr.length);
|
|
39
|
+
return {
|
|
40
|
+
topic,
|
|
41
|
+
totalQuestions: total,
|
|
42
|
+
questionClusters: clusters
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function clusterKeywords(keywords, similarityThreshold = 0.6) {
|
|
46
|
+
const clusters = [];
|
|
47
|
+
const assigned = new Set();
|
|
48
|
+
keywords.forEach(kw => {
|
|
49
|
+
if (assigned.has(kw))
|
|
50
|
+
return;
|
|
51
|
+
const words = kw.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
52
|
+
const related = [];
|
|
53
|
+
keywords.forEach(other => {
|
|
54
|
+
if (other === kw || assigned.has(other))
|
|
55
|
+
return;
|
|
56
|
+
const otherWords = other.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
57
|
+
// Jaccard similarity between words
|
|
58
|
+
const intersection = words.filter(w => otherWords.includes(w)).length;
|
|
59
|
+
const union = new Set([...words, ...otherWords]).size;
|
|
60
|
+
const sim = union > 0 ? intersection / union : 0;
|
|
61
|
+
if (sim >= similarityThreshold || (words.length > 1 && other.toLowerCase().includes(words[0]))) {
|
|
62
|
+
related.push(other);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
assigned.add(kw);
|
|
66
|
+
related.forEach(r => assigned.add(r));
|
|
67
|
+
const pillar = kw;
|
|
68
|
+
const slug = pillar.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
69
|
+
clusters.push({
|
|
70
|
+
clusterName: `${pillar} Topic Hub`,
|
|
71
|
+
pillarTopic: pillar,
|
|
72
|
+
primaryKeyword: pillar,
|
|
73
|
+
supportingKeywords: related,
|
|
74
|
+
recommendedArticleType: related.length > 2 ? 'Pillar Guide with Cluster Subpages' : 'Standalone Target Article',
|
|
75
|
+
recommendedUrlSlug: `/${slug}`
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
totalKeywords: keywords.length,
|
|
80
|
+
clusterCount: clusters.length,
|
|
81
|
+
clusters: clusters.sort((a, b) => b.supportingKeywords.length - a.supportingKeywords.length)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function classifySearchIntent(keywords) {
|
|
85
|
+
return keywords.map(kw => {
|
|
86
|
+
const lower = kw.toLowerCase();
|
|
87
|
+
let intent = 'Informational';
|
|
88
|
+
let confidence = 0.85;
|
|
89
|
+
let format = 'In-depth Tutorial / Guide / Explanation';
|
|
90
|
+
if (/\b(buy|order|discount|coupon|deal|pricing|price|cost|shop|purchase)\b/i.test(lower)) {
|
|
91
|
+
intent = 'Transactional';
|
|
92
|
+
confidence = 0.95;
|
|
93
|
+
format = 'Product / Checkout / Pricing Page with direct Buy CTAs';
|
|
94
|
+
}
|
|
95
|
+
else if (/\b(best|top|review|reviews|vs|versus|comparison|alternative|alternatives)\b/i.test(lower)) {
|
|
96
|
+
intent = 'Commercial Investigation';
|
|
97
|
+
confidence = 0.92;
|
|
98
|
+
format = 'Comparison Matrix / Roundup Review Table with pros & cons';
|
|
99
|
+
}
|
|
100
|
+
else if (/\b(login|sign in|portal|official|app|account|status)\b/i.test(lower)) {
|
|
101
|
+
intent = 'Navigational';
|
|
102
|
+
confidence = 0.90;
|
|
103
|
+
format = 'Landing Page / Direct Portal Link';
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
keyword: kw,
|
|
107
|
+
intent,
|
|
108
|
+
confidenceScore: confidence,
|
|
109
|
+
recommendedPageFormat: format
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OnPageAuditReport, ContentBrief } from '../types/seo.js';
|
|
2
|
+
export declare function auditOnPage(urlOrHtml: string, focusKeyword?: string): Promise<OnPageAuditReport>;
|
|
3
|
+
export declare function generateContentBrief(primaryKeyword: string, secondaryKeywords?: string[], searchIntent?: 'Informational' | 'Transactional' | 'Commercial Investigation' | 'Navigational'): Promise<ContentBrief>;
|
|
4
|
+
export declare function scoreReadability(textOrUrl: string): Promise<{
|
|
5
|
+
fleschReadingEase: number;
|
|
6
|
+
gradeLevel: number;
|
|
7
|
+
gunningFog: number;
|
|
8
|
+
readingLevelSummary: string;
|
|
9
|
+
totalWords: number;
|
|
10
|
+
totalSentences: number;
|
|
11
|
+
wordsPerSentence: number;
|
|
12
|
+
passiveVoicePercent: number;
|
|
13
|
+
longSentencesCount: number;
|
|
14
|
+
sampleLongSentences: string[];
|
|
15
|
+
}>;
|