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.
@@ -0,0 +1,354 @@
1
+ export interface SerpResultItem {
2
+ rank: number;
3
+ title: string;
4
+ url: string;
5
+ snippet: string;
6
+ displayedBreadcrumbs?: string;
7
+ isFeaturedSnippet?: boolean;
8
+ }
9
+ export interface SerpAnalysisResponse {
10
+ query: string;
11
+ totalResultsFound?: string;
12
+ organicResults: SerpResultItem[];
13
+ peopleAlsoAsk: string[];
14
+ relatedSearches: string[];
15
+ serpFeaturesDetected: string[];
16
+ }
17
+ export interface CompetitorPageProfile {
18
+ url: string;
19
+ title: string;
20
+ description: string;
21
+ h1: string[];
22
+ h2: string[];
23
+ h3: string[];
24
+ h4: string[];
25
+ wordCount: number;
26
+ readingTimeMinutes: number;
27
+ readingGradeLevel: string;
28
+ schemasFound: string[];
29
+ canonical: string;
30
+ robotsDirectives: string;
31
+ openGraph: Record<string, string>;
32
+ imageCount: number;
33
+ imagesMissingAlt: number;
34
+ internalLinksCount: number;
35
+ externalLinksCount: number;
36
+ }
37
+ export interface ContentGapAnalysis {
38
+ targetUrlOrKeyword: string;
39
+ analyzedCompetitors: string[];
40
+ averageCompetitorWordCount: number;
41
+ targetWordCount: number;
42
+ wordCountDelta: number;
43
+ missingEntities: Array<{
44
+ term: string;
45
+ competitorFrequency: number;
46
+ targetFrequency: number;
47
+ importance: 'high' | 'medium' | 'low';
48
+ }>;
49
+ headingCoverageGaps: Array<{
50
+ subtopic: string;
51
+ coveredByCompetitorUrls: string[];
52
+ }>;
53
+ suggestedActionItems: string[];
54
+ }
55
+ export interface CompetitorDiffMatrix {
56
+ myUrl: string;
57
+ competitorUrl: string;
58
+ focusKeyword: string;
59
+ scorecard: Array<{
60
+ metric: string;
61
+ myValue: string | number;
62
+ competitorValue: string | number;
63
+ winner: 'my_site' | 'competitor' | 'tie';
64
+ notes: string;
65
+ }>;
66
+ summary: {
67
+ myScore: number;
68
+ competitorScore: number;
69
+ winner: 'my_site' | 'competitor' | 'tie';
70
+ topPriorityFixes: string[];
71
+ };
72
+ }
73
+ export interface ForumDiscussionsPulse {
74
+ topic: string;
75
+ rankingDiscussions: Array<{
76
+ platform: 'Reddit' | 'Quora' | 'Other Forum';
77
+ title: string;
78
+ url: string;
79
+ snippet: string;
80
+ }>;
81
+ extractedThemes: string[];
82
+ frequentUserPainPoints: string[];
83
+ consensusRecommendations: string[];
84
+ }
85
+ export interface GeoAiReadinessReport {
86
+ targetQuery: string;
87
+ overallGeoScore: number;
88
+ citationLikelihood: 'High' | 'Medium' | 'Low';
89
+ checks: {
90
+ directAnswerParagraph: {
91
+ passed: boolean;
92
+ score: number;
93
+ feedback: string;
94
+ detectedSnippet?: string;
95
+ };
96
+ semanticChunking: {
97
+ passed: boolean;
98
+ score: number;
99
+ feedback: string;
100
+ };
101
+ structuredDataAndLists: {
102
+ passed: boolean;
103
+ score: number;
104
+ feedback: string;
105
+ };
106
+ authoritativeCitationsAndStats: {
107
+ passed: boolean;
108
+ score: number;
109
+ feedback: string;
110
+ detectedStatsCount: number;
111
+ };
112
+ entityClarity: {
113
+ passed: boolean;
114
+ score: number;
115
+ feedback: string;
116
+ };
117
+ };
118
+ recommendedSnippetsForAiCitation: Array<{
119
+ section: string;
120
+ suggestedFormat: string;
121
+ exampleText: string;
122
+ }>;
123
+ }
124
+ export interface InformationGainReport {
125
+ targetKeyword: string;
126
+ informationGainScore: number;
127
+ noveltyTier: 'Exceptional (High Information Gain)' | 'Moderate' | 'Low (Rehashed / Generic AI Risk)';
128
+ uniqueEntitiesDetected: string[];
129
+ competitorOverlapPercentage: number;
130
+ originalElementsFound: {
131
+ dataPointsAndStats: string[];
132
+ caseStudiesOrExamples: string[];
133
+ uniqueMethodologiesOrQuotes: string[];
134
+ };
135
+ recommendationsToIncreaseGain: string[];
136
+ }
137
+ export interface EeatAuditReport {
138
+ overallEeatScore: number;
139
+ trustLevel: 'High Authority' | 'Moderate' | 'Needs Improvement';
140
+ signals: {
141
+ authorIdentity: {
142
+ hasAuthorByline: boolean;
143
+ hasAuthorBio: boolean;
144
+ hasPersonSchema: boolean;
145
+ sameAsProfilesLinked: string[];
146
+ };
147
+ transparency: {
148
+ hasEditorialPolicy: boolean;
149
+ hasFactCheckDisclaimer: boolean;
150
+ hasPublishDate: boolean;
151
+ hasModifiedDate: boolean;
152
+ };
153
+ contactAndEntity: {
154
+ hasAboutPageLink: boolean;
155
+ hasContactInfo: boolean;
156
+ hasPhysicalAddress: boolean;
157
+ };
158
+ citationsAndReferences: {
159
+ externalAuthoritativeCitationsCount: number;
160
+ peerReviewedOrGovLinksCount: number;
161
+ };
162
+ };
163
+ actionableImprovements: string[];
164
+ }
165
+ export interface OnPageAuditReport {
166
+ urlOrTitle: string;
167
+ focusKeyword?: string;
168
+ overallScore: number;
169
+ titleAudit: {
170
+ text: string;
171
+ characterCount: number;
172
+ estimatedPixelWidth: number;
173
+ status: 'optimal' | 'too_short' | 'too_long';
174
+ containsKeyword: boolean;
175
+ recommendation?: string;
176
+ };
177
+ metaDescriptionAudit: {
178
+ text: string;
179
+ characterCount: number;
180
+ status: 'optimal' | 'too_short' | 'too_long' | 'missing';
181
+ containsKeyword: boolean;
182
+ hasCallToAction: boolean;
183
+ recommendation?: string;
184
+ };
185
+ headingsAudit: {
186
+ h1Count: number;
187
+ h1Texts: string[];
188
+ h2Count: number;
189
+ h3Count: number;
190
+ hierarchyValid: boolean;
191
+ keywordInH1: boolean;
192
+ keywordInH2: boolean;
193
+ issues: string[];
194
+ };
195
+ contentBodyAudit: {
196
+ wordCount: number;
197
+ keywordOccurrences: number;
198
+ keywordDensityPercent: number;
199
+ keywordInFirst100Words: boolean;
200
+ readabilityGrade: string;
201
+ };
202
+ imagesAudit: {
203
+ totalImages: number;
204
+ missingAltCount: number;
205
+ imagesWithAltCount: number;
206
+ suspiciouslyLargeImages: string[];
207
+ };
208
+ linksAudit: {
209
+ internalLinksCount: number;
210
+ externalLinksCount: number;
211
+ genericAnchorTextsFound: string[];
212
+ };
213
+ urlSlugAudit: {
214
+ slug: string;
215
+ hasStopWords: boolean;
216
+ containsKeyword: boolean;
217
+ status: 'optimal' | 'warning';
218
+ };
219
+ }
220
+ export interface ContentBrief {
221
+ primaryKeyword: string;
222
+ secondaryKeywords: string[];
223
+ searchIntent: 'Informational' | 'Transactional' | 'Commercial Investigation' | 'Navigational';
224
+ recommendedWordCount: {
225
+ min: number;
226
+ target: number;
227
+ max: number;
228
+ };
229
+ recommendedTitleFormulas: string[];
230
+ recommendedMetaDescriptions: string[];
231
+ headingOutline: Array<{
232
+ level: 'H1' | 'H2' | 'H3';
233
+ text: string;
234
+ intentNotes: string;
235
+ suggestedEntitiesToMention: string[];
236
+ }>;
237
+ requiredSemanticEntities: string[];
238
+ peopleAlsoAskFaqSection: Array<{
239
+ question: string;
240
+ suggestedAnswerBullets: string[];
241
+ }>;
242
+ }
243
+ export interface JsRenderingDiffReport {
244
+ url: string;
245
+ serverHtmlLength: number;
246
+ hydratedDomLength: number;
247
+ contentDifferencePercent: number;
248
+ jsDependentElements: {
249
+ linksOnlyInClientDom: string[];
250
+ headingsOnlyInClientDom: string[];
251
+ metaTagsRewrittenByClient: Array<{
252
+ tag: string;
253
+ serverValue: string;
254
+ clientValue: string;
255
+ }>;
256
+ };
257
+ seoCrawlerRisk: 'Low' | 'Medium' | 'High (Significant Hydration Dependence)';
258
+ recommendations: string[];
259
+ }
260
+ export interface TechnicalAuditReport {
261
+ url: string;
262
+ statusCode: number;
263
+ responseTimeMs: number;
264
+ redirectChain: string[];
265
+ isHttps: boolean;
266
+ canonicalTag: {
267
+ present: boolean;
268
+ value: string;
269
+ isSelfReferencing: boolean;
270
+ };
271
+ robotsDirectives: {
272
+ metaRobots: string;
273
+ xRobotsTag: string;
274
+ isNoIndex: boolean;
275
+ isNoFollow: boolean;
276
+ };
277
+ hreflangTags: Array<{
278
+ lang: string;
279
+ href: string;
280
+ }>;
281
+ openGraphTags: {
282
+ hasTitle: boolean;
283
+ hasDescription: boolean;
284
+ hasImage: boolean;
285
+ hasType: boolean;
286
+ };
287
+ twitterCards: {
288
+ hasCard: boolean;
289
+ hasTitle: boolean;
290
+ hasImage: boolean;
291
+ };
292
+ issuesFound: Array<{
293
+ severity: 'critical' | 'warning' | 'info';
294
+ message: string;
295
+ fix: string;
296
+ }>;
297
+ }
298
+ export interface EntitySalienceItem {
299
+ name: string;
300
+ salienceScore: number;
301
+ type: string;
302
+ wikidataId?: string;
303
+ contextSentence?: string;
304
+ }
305
+ export interface SpoTriple {
306
+ subject: string;
307
+ predicate: string;
308
+ object: string;
309
+ sourceSentence: string;
310
+ }
311
+ export interface EntitySalienceMapReport {
312
+ totalEntitiesFound: number;
313
+ topEntities: EntitySalienceItem[];
314
+ semanticTriples: SpoTriple[];
315
+ knowledgeGraphSummary: string;
316
+ }
317
+ export interface SchemaValidationResult {
318
+ schemasDetectedCount: number;
319
+ schemas: Array<{
320
+ type: string;
321
+ rawObject: Record<string, any>;
322
+ isValid: boolean;
323
+ missingMandatoryFields: string[];
324
+ recommendedImprovements: string[];
325
+ }>;
326
+ googleRichResultEligibility: Array<{
327
+ feature: string;
328
+ eligible: boolean;
329
+ missingRequirements: string[];
330
+ }>;
331
+ }
332
+ export interface KeywordClusterGroup {
333
+ clusterName: string;
334
+ pillarTopic: string;
335
+ primaryKeyword: string;
336
+ supportingKeywords: string[];
337
+ recommendedArticleType: string;
338
+ recommendedUrlSlug: string;
339
+ }
340
+ export interface SearchIntentClassification {
341
+ keyword: string;
342
+ intent: 'Informational' | 'Transactional' | 'Commercial Investigation' | 'Navigational';
343
+ confidenceScore: number;
344
+ recommendedPageFormat: string;
345
+ }
346
+ export interface ContentDecayReport {
347
+ urlOrTitle: string;
348
+ freshnessScore: number;
349
+ decayLevel: 'Fresh' | 'Mild Decay' | 'Severe Decay';
350
+ staleYearReferences: string[];
351
+ detectedOutdatedStats: string[];
352
+ brokenOutboundLinks: string[];
353
+ suggestedUpdateChecklist: string[];
354
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { JsRenderingDiffReport } from '../types/seo.js';
2
+ /**
3
+ * Compares initial server HTML with the rendered DOM (JavaScript SEO Hydration diffing).
4
+ */
5
+ export declare function compareServerVsClientDom(url: string): Promise<JsRenderingDiffReport>;
@@ -0,0 +1,79 @@
1
+ import { JSDOM, VirtualConsole } from 'jsdom';
2
+ import axios from 'axios';
3
+ import { getRandomUserAgent } from './scraper.js';
4
+ /**
5
+ * Compares initial server HTML with the rendered DOM (JavaScript SEO Hydration diffing).
6
+ */
7
+ export async function compareServerVsClientDom(url) {
8
+ let serverHtml = '';
9
+ try {
10
+ const response = await axios.get(url, {
11
+ headers: {
12
+ 'User-Agent': getRandomUserAgent(),
13
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
14
+ },
15
+ timeout: 12000,
16
+ validateStatus: () => true
17
+ });
18
+ serverHtml = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
19
+ }
20
+ catch (err) {
21
+ throw new Error(`Failed to fetch server HTML from ${url}: ${err.message}`);
22
+ }
23
+ // Render in JSDOM with script execution enabled
24
+ const virtualConsole = new VirtualConsole();
25
+ virtualConsole.on('error', () => { }); // silence js console errors
26
+ virtualConsole.on('warn', () => { });
27
+ let hydratedDomHtml = serverHtml;
28
+ try {
29
+ const dom = new JSDOM(serverHtml, {
30
+ url,
31
+ runScripts: 'outside-only',
32
+ resources: 'usable',
33
+ virtualConsole
34
+ });
35
+ hydratedDomHtml = dom.serialize();
36
+ }
37
+ catch {
38
+ hydratedDomHtml = serverHtml;
39
+ }
40
+ const serverLength = serverHtml.length;
41
+ const clientLength = hydratedDomHtml.length;
42
+ const lengthDiff = Math.abs(clientLength - serverLength);
43
+ const percentDiff = Number(((lengthDiff / Math.max(serverLength, 1)) * 100).toFixed(1));
44
+ // Inspect link and heading differences
45
+ const serverLinks = Array.from(serverHtml.matchAll(/href=["'](https?:\/\/[^"']+|\/[^"']+)["']/gi)).map(m => m[1]);
46
+ const clientLinks = Array.from(hydratedDomHtml.matchAll(/href=["'](https?:\/\/[^"']+|\/[^"']+)["']/gi)).map(m => m[1]);
47
+ const linksOnlyInClient = clientLinks.filter(l => !serverLinks.includes(l)).slice(0, 10);
48
+ const serverH1s = Array.from(serverHtml.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/gi)).map(m => m[1].replace(/<[^>]+>/g, '').trim());
49
+ const clientH1s = Array.from(hydratedDomHtml.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/gi)).map(m => m[1].replace(/<[^>]+>/g, '').trim());
50
+ const headingsOnlyInClient = clientH1s.filter(h => !serverH1s.includes(h));
51
+ const crawlerRisk = linksOnlyInClient.length > 5 || percentDiff > 50
52
+ ? 'High (Significant Hydration Dependence)'
53
+ : percentDiff > 20
54
+ ? 'Medium'
55
+ : 'Low';
56
+ const recommendations = [];
57
+ if (linksOnlyInClient.length > 0) {
58
+ recommendations.push(`Ensure critical navigation links (${linksOnlyInClient.length} detected) are present in the initial Server-Side Rendered (SSR) HTML rather than injected via client JavaScript.`);
59
+ }
60
+ if (headingsOnlyInClient.length > 0) {
61
+ recommendations.push('H1 tag is rendered via client-side JavaScript. Pre-render H1 in server HTML to guarantee immediate crawler indexing.');
62
+ }
63
+ if (percentDiff < 15 && linksOnlyInClient.length === 0) {
64
+ recommendations.push('Excellent hydration parity. Initial HTML matches rendered DOM cleanly for search engine bots.');
65
+ }
66
+ return {
67
+ url,
68
+ serverHtmlLength: serverLength,
69
+ hydratedDomLength: clientLength,
70
+ contentDifferencePercent: percentDiff,
71
+ jsDependentElements: {
72
+ linksOnlyInClientDom: linksOnlyInClient,
73
+ headingsOnlyInClientDom: headingsOnlyInClient,
74
+ metaTagsRewrittenByClient: []
75
+ },
76
+ seoCrawlerRisk: crawlerRisk,
77
+ recommendations
78
+ };
79
+ }
@@ -0,0 +1,41 @@
1
+ import { SpoTriple, EntitySalienceItem } from '../types/seo.js';
2
+ /**
3
+ * Extract meaningful 1-gram, 2-gram, and 3-gram keyphrases and calculate their frequencies.
4
+ */
5
+ export declare function extractKeyphrases(text: string, maxItems?: number): Array<{
6
+ term: string;
7
+ count: number;
8
+ tf: number;
9
+ }>;
10
+ /**
11
+ * Computes TF-IDF between target text and a set of competitor texts to detect content gaps.
12
+ */
13
+ export declare function computeContentGapTfIdf(targetText: string, competitorTexts: string[]): Array<{
14
+ term: string;
15
+ competitorFrequency: number;
16
+ targetFrequency: number;
17
+ importance: 'high' | 'medium' | 'low';
18
+ }>;
19
+ /**
20
+ * Calculates Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog index.
21
+ */
22
+ export declare function calculateReadability(text: string): {
23
+ fleschReadingEase: number;
24
+ gradeLevel: number;
25
+ gunningFog: number;
26
+ readingLevelSummary: string;
27
+ totalWords: number;
28
+ totalSentences: number;
29
+ wordsPerSentence: number;
30
+ passiveVoicePercent: number;
31
+ longSentencesCount: number;
32
+ sampleLongSentences: string[];
33
+ };
34
+ /**
35
+ * Extracts Subject-Predicate-Object (SPO) semantic triples from text for Knowledge Graph analysis.
36
+ */
37
+ export declare function extractSpoTriples(text: string): SpoTriple[];
38
+ /**
39
+ * Extracts key named entities with approximate salience scoring.
40
+ */
41
+ export declare function extractEntitiesWithSalience(text: string): EntitySalienceItem[];