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,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
+ }>;