@lynxflow/seo-engine 1.8.30 → 2.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.
Files changed (96) hide show
  1. package/README.md +47 -93
  2. package/dist/ai-copilot-client.d.ts +2 -0
  3. package/dist/built-in-locations.d.ts +18 -0
  4. package/dist/content-ai-arsenal.d.ts +28 -0
  5. package/dist/existing-media-harvester.d.ts +33 -0
  6. package/dist/extended-schemas.d.ts +84 -0
  7. package/dist/features-knowledge-harvester.d.ts +56 -0
  8. package/dist/geo-citation-scorer.d.ts +34 -0
  9. package/dist/i18n-detector.d.ts +1 -1
  10. package/dist/index.d.ts +32 -7
  11. package/dist/index.js +538 -82
  12. package/dist/index.mjs +2028 -163
  13. package/dist/internal-pagerank-graph.d.ts +59 -0
  14. package/dist/knowledge-bank-builder.d.ts +91 -0
  15. package/dist/marketing-skills-engine.d.ts +74 -0
  16. package/dist/matrix-engine.d.ts +26 -0
  17. package/dist/power-words-psychology.d.ts +27 -15
  18. package/dist/public-manifest-engine.d.ts +58 -0
  19. package/dist/rag-knowledge-engine.d.ts +50 -0
  20. package/dist/rate-limited-translator.d.ts +19 -0
  21. package/dist/token-quota-manager.d.ts +4 -0
  22. package/package.json +11 -3
  23. package/TUTORIEL_INTEGRATION_SITE.md +0 -283
  24. package/connectors/cloudflare-worker/worker.js +0 -106
  25. package/connectors/laravel/LynxSeoController.php +0 -37
  26. package/connectors/wordpress/lynxseo-admin-app.js +0 -2611
  27. package/connectors/wordpress/lynxseo-connector.php +0 -1712
  28. package/connectors/wordpress/lynxseo-connector.zip +0 -0
  29. package/lynxflow-seo-engine-1.2.0.tgz +0 -0
  30. package/src/ad-intelligence-cro.ts +0 -140
  31. package/src/ai-bots-log-analyzer.ts +0 -127
  32. package/src/ai-copilot-client.ts +0 -99
  33. package/src/analytics-client.ts +0 -315
  34. package/src/auth-key.ts +0 -262
  35. package/src/backlinks-client.ts +0 -85
  36. package/src/brand-dna-calendar.ts +0 -120
  37. package/src/brand-icons.ts +0 -138
  38. package/src/built-in-locations.ts +0 -170
  39. package/src/content-ai-arsenal.ts +0 -268
  40. package/src/copy-frameworks-master.ts +0 -89
  41. package/src/cro-copywriting-engine.ts +0 -105
  42. package/src/crosslink-scorer.ts +0 -101
  43. package/src/embeddable-seo-widget.ts +0 -57
  44. package/src/engine.test.ts +0 -386
  45. package/src/engine.ts +0 -813
  46. package/src/existing-media-harvester.ts +0 -98
  47. package/src/extended-schemas.ts +0 -589
  48. package/src/features-knowledge-harvester.ts +0 -190
  49. package/src/free-public-tools.ts +0 -224
  50. package/src/geo-mesh-linking.ts +0 -93
  51. package/src/google-business-profile.ts +0 -280
  52. package/src/i18n-detector.ts +0 -98
  53. package/src/i18n-dictionary.ts +0 -422
  54. package/src/index.ts +0 -164
  55. package/src/indexnow-client.ts +0 -94
  56. package/src/instant-matrix-search.ts +0 -48
  57. package/src/isr-cache-manager.ts +0 -78
  58. package/src/keyword-permutator.ts +0 -118
  59. package/src/knowledge-bank-builder.ts +0 -299
  60. package/src/knowledge-graph-linker.ts +0 -81
  61. package/src/lago-token-meter.ts +0 -120
  62. package/src/legal-disclaimers.ts +0 -407
  63. package/src/llm-content-cleaner.ts +0 -64
  64. package/src/llm-prompt.ts +0 -323
  65. package/src/master-marketing-engine.ts +0 -314
  66. package/src/matrix-engine.ts +0 -1204
  67. package/src/mcp-seo-server.ts +0 -108
  68. package/src/ngram-density-analyzer.ts +0 -108
  69. package/src/og-image-generator.ts +0 -109
  70. package/src/power-words-psychology.ts +0 -369
  71. package/src/public-manifest-engine.ts +0 -109
  72. package/src/rag-knowledge-engine.ts +0 -163
  73. package/src/rank-math-parity.ts +0 -235
  74. package/src/rate-limited-translator.ts +0 -110
  75. package/src/real-reviews-sync.ts +0 -207
  76. package/src/rss-syndication-feed.ts +0 -56
  77. package/src/schema-builder.ts +0 -134
  78. package/src/seo-opportunities-decay.ts +0 -154
  79. package/src/serp-client.ts +0 -89
  80. package/src/serp-history-alerts.ts +0 -75
  81. package/src/site-auditor.ts +0 -83
  82. package/src/site-crawler.ts +0 -406
  83. package/src/slug-engine.ts +0 -183
  84. package/src/social-ads-seo.ts +0 -211
  85. package/src/social-growth-suite.ts +0 -310
  86. package/src/social-trend-seo.ts +0 -103
  87. package/src/social-video-seo.ts +0 -100
  88. package/src/team-rbac.ts +0 -61
  89. package/src/technical-rules-auditor.ts +0 -171
  90. package/src/token-quota-manager.ts +0 -91
  91. package/src/types.ts +0 -143
  92. package/src/ui-icons.ts +0 -134
  93. package/src/urlytics-engine.ts +0 -97
  94. package/src/video-youtube-analyzer.ts +0 -81
  95. package/src/yoast-parity.ts +0 -161
  96. package/tsconfig.json +0 -15
@@ -1,134 +0,0 @@
1
- /**
2
- * 🌟 Standalone Schema.org Graph Builder
3
- *
4
- * Generates verified JSON-LD graphs (Product, FAQPage, Organization, Speakable)
5
- * for any manual or dynamic page without requiring the full engine resolver.
6
- */
7
-
8
- export class SchemaGraphBuilder {
9
- /**
10
- * Generates a complete Schema.org Graph for Product + FAQ + Speakable.
11
- */
12
- static buildGraph(opts: {
13
- url: string;
14
- name: string;
15
- description: string;
16
- brandName: string;
17
- price?: number;
18
- currency?: string;
19
- image?: string;
20
- faqs?: { question: string; answer: string }[];
21
- ratingValue?: string;
22
- reviewCount?: string;
23
- locale?: string;
24
- reviews?: Array<{
25
- authorName: string;
26
- ratingValue: number;
27
- reviewText: string;
28
- datePublished?: string;
29
- }>;
30
- }): Record<string, unknown> {
31
- const productNode: Record<string, unknown> = {
32
- "@type": "Product",
33
- "@id": `${opts.url}#product`,
34
- name: opts.name,
35
- description: opts.description,
36
- image: opts.image || `${opts.url}/api/og`,
37
- brand: {
38
- "@type": "Brand",
39
- name: opts.brandName,
40
- },
41
- };
42
-
43
- if (opts.locale) {
44
- productNode.inLanguage = opts.locale;
45
- }
46
-
47
- // Automatically compute or attach real AggregateRating
48
- if (opts.ratingValue && opts.reviewCount) {
49
- productNode.aggregateRating = {
50
- "@type": "AggregateRating",
51
- ratingValue: opts.ratingValue,
52
- reviewCount: opts.reviewCount,
53
- bestRating: "5",
54
- worstRating: "1",
55
- };
56
- } else if (opts.reviews && opts.reviews.length > 0) {
57
- const avg = (
58
- opts.reviews.reduce((acc, r) => acc + r.ratingValue, 0) / opts.reviews.length
59
- ).toFixed(1);
60
- productNode.aggregateRating = {
61
- "@type": "AggregateRating",
62
- ratingValue: avg,
63
- reviewCount: opts.reviews.length.toString(),
64
- bestRating: "5",
65
- worstRating: "1",
66
- };
67
- }
68
-
69
- // Attach individual authentic reviews if provided
70
- if (opts.reviews && opts.reviews.length > 0) {
71
- productNode.review = opts.reviews.map((r) => ({
72
- "@type": "Review",
73
- author: {
74
- "@type": "Person",
75
- name: r.authorName,
76
- },
77
- datePublished: r.datePublished || new Date().toISOString().split("T")[0],
78
- reviewBody: r.reviewText,
79
- reviewRating: {
80
- "@type": "Rating",
81
- ratingValue: r.ratingValue.toString(),
82
- bestRating: "5",
83
- worstRating: "1",
84
- },
85
- }));
86
- }
87
-
88
- // Only emit Offer if real price is provided
89
- if (opts.price !== undefined) {
90
- productNode.offers = {
91
- "@type": "Offer",
92
- price: opts.price.toString(),
93
- priceCurrency: opts.currency || "EUR",
94
- availability: "https://schema.org/InStock",
95
- url: opts.url,
96
- };
97
- }
98
-
99
- const graph: Record<string, unknown>[] = [productNode];
100
-
101
- if (opts.faqs && opts.faqs.length > 0) {
102
- graph.push({
103
- "@type": "FAQPage",
104
- "@id": `${opts.url}#faq`,
105
- inLanguage: opts.locale || "fr",
106
- mainEntity: opts.faqs.map((f) => ({
107
- "@type": "Question",
108
- name: f.question,
109
- acceptedAnswer: {
110
- "@type": "Answer",
111
- text: f.answer,
112
- },
113
- })),
114
- });
115
- }
116
-
117
- graph.push({
118
- "@type": "WebPage",
119
- "@id": `${opts.url}#webpage`,
120
- url: opts.url,
121
- name: opts.name,
122
- description: opts.description,
123
- speakable: {
124
- "@type": "SpeakableSpecification",
125
- cssSelector: [".geo-direct-answer", "h1"],
126
- },
127
- });
128
-
129
- return {
130
- "@context": "https://schema.org",
131
- "@graph": graph,
132
- };
133
- }
134
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * 🎯 Advanced SEO Opportunities & Content Decay Detector (Inspiration: crawlseo/seo-opportunities.ts)
3
- *
4
- * Implements mathematical models to detect:
5
- * 1. Striking Distance Keywords (Positions 4 to 20 with high impressions)
6
- * 2. Low CTR Gaps (Real CTR < Expected Industry Curve)
7
- * 3. Content Decay (Pages losing > 30% traffic over 28-day windows)
8
- * 4. Keyword Cannibalization (Multiple internal URLs splitting Google rankings)
9
- */
10
-
11
- export interface KeywordMetric {
12
- query: string;
13
- url: string;
14
- position: number;
15
- clicks: number;
16
- impressions: number;
17
- ctr: number;
18
- }
19
-
20
- export interface ContentDecayInput {
21
- url: string;
22
- previous28DaysClicks: number;
23
- last28DaysClicks: number;
24
- }
25
-
26
- export interface OpportunityFinding {
27
- type: "striking_distance" | "low_ctr" | "content_decay" | "cannibalization";
28
- severity: "critical" | "high" | "medium" | "low";
29
- title: string;
30
- detail: string;
31
- query?: string;
32
- url?: string;
33
- potentialTrafficGain?: number;
34
- suggestedAction: string;
35
- }
36
-
37
- export class SeoOpportunitiesDecayDetector {
38
- /**
39
- * Expected organic CTR curve by Google SERP rank position.
40
- */
41
- static getExpectedCtr(position: number): number {
42
- if (position <= 1) return 0.28;
43
- if (position <= 2) return 0.15;
44
- if (position <= 3) return 0.11;
45
- if (position <= 5) return 0.07;
46
- if (position <= 10) return 0.03;
47
- if (position <= 20) return 0.01;
48
- return 0.005;
49
- }
50
-
51
- /**
52
- * Detects Striking Distance opportunities (Keywords ranked between #4 and #20).
53
- */
54
- static detectStrikingDistance(keywords: KeywordMetric[], minImpressions = 30): OpportunityFinding[] {
55
- return keywords
56
- .filter((k) => k.position >= 4 && k.position <= 20 && k.impressions >= minImpressions)
57
- .sort((a, b) => b.impressions - a.impressions)
58
- .map((k) => {
59
- const targetPos = Math.max(1, k.position - 3);
60
- const potentialClicks = Math.round(k.impressions * this.getExpectedCtr(targetPos)) - k.clicks;
61
-
62
- return {
63
- type: "striking_distance",
64
- severity: k.position <= 10 ? "high" : "medium",
65
- title: `🎯 Mot-clé à portée de main : "${k.query}" (#${k.position.toFixed(1)})`,
66
- detail: `${k.impressions} impressions mensuelles actuelles. Atteindre le top 3 générerait environ +${Math.max(1, potentialClicks)} clics/mois.`,
67
- query: k.query,
68
- url: k.url,
69
- potentialTrafficGain: Math.max(1, potentialClicks),
70
- suggestedAction: "Ajoutez 1 lien interne contextualisé vers cette page et enrichissez la section FAQ avec ce terme.",
71
- };
72
- });
73
- }
74
-
75
- /**
76
- * Detects Low CTR Gaps (Ranked in top 15 but underperforming in clicks).
77
- */
78
- static detectLowCtrGaps(keywords: KeywordMetric[], minImpressions = 50): OpportunityFinding[] {
79
- return keywords
80
- .filter((k) => k.impressions >= minImpressions && k.position <= 15)
81
- .map((k) => {
82
- const expected = this.getExpectedCtr(k.position);
83
- const gap = expected - k.ctr;
84
- return { ...k, expected, gap };
85
- })
86
- .filter((k) => k.gap > 0.02)
87
- .sort((a, b) => b.impressions * b.gap - a.impressions * a.gap)
88
- .map((k) => ({
89
- type: "low_ctr" as const,
90
- severity: "high" as const,
91
- title: `⚡ CTR anormalement bas sur "${k.query}" (${(k.ctr * 100).toFixed(1)}% vs ${(k.expected * 100).toFixed(1)}% attendu)`,
92
- detail: `Classé #${k.position.toFixed(1)} avec ${k.impressions} impressions, mais un taux de clic inférieur aux moyennes du secteur.`,
93
- query: k.query,
94
- url: k.url,
95
- potentialTrafficGain: Math.round(k.impressions * k.gap),
96
- suggestedAction: "Réécrivez la balise <title> et la méta-description avec des mots d'action, des chiffres ou un crochet d'actualité.",
97
- }));
98
- }
99
-
100
- /**
101
- * Detects Content Decay (Pages losing more than 30% of traffic).
102
- */
103
- static detectContentDecay(pages: ContentDecayInput[]): OpportunityFinding[] {
104
- return pages
105
- .filter((p) => p.previous28DaysClicks >= 20)
106
- .map((p) => {
107
- const dropRatio = (p.previous28DaysClicks - p.last28DaysClicks) / p.previous28DaysClicks;
108
- return { ...p, dropRatio };
109
- })
110
- .filter((p) => p.dropRatio >= 0.3)
111
- .sort((a, b) => b.dropRatio - a.dropRatio)
112
- .map((p) => ({
113
- type: "content_decay" as const,
114
- severity: "critical" as const,
115
- title: `📉 Déclin de contenu détecté (-${Math.round(p.dropRatio * 100)}% de trafic)`,
116
- detail: `Le trafic est passé de ${p.previous28DaysClicks} à ${p.last28DaysClicks} clics sur les 28 derniers jours.`,
117
- url: p.url,
118
- potentialTrafficGain: p.previous28DaysClicks - p.last28DaysClicks,
119
- suggestedAction: "Mettez à jour les données datées, ajoutez une section 2026 et vérifiez si des concurrents ont publié un meilleur guide.",
120
- }));
121
- }
122
-
123
- /**
124
- * Detects Keyword Cannibalization across internal URLs.
125
- */
126
- static detectCannibalization(keywords: KeywordMetric[]): OpportunityFinding[] {
127
- const queryMap: Record<string, KeywordMetric[]> = {};
128
-
129
- for (const k of keywords) {
130
- if (!queryMap[k.query]) queryMap[k.query] = [];
131
- queryMap[k.query].push(k);
132
- }
133
-
134
- const cannibalized: OpportunityFinding[] = [];
135
-
136
- for (const [query, urls] of Object.entries(queryMap)) {
137
- if (urls.length > 1) {
138
- const totalImpressions = urls.reduce((acc, curr) => acc + curr.impressions, 0);
139
- if (totalImpressions >= 20) {
140
- cannibalized.push({
141
- type: "cannibalization",
142
- severity: "high",
143
- title: `⚔️ Cannibalisation détectée sur "${query}" (${urls.length} URLs en compétition)`,
144
- detail: `Les URLs suivantes se partagent les clics : ${urls.map((u) => u.url).join(", ")}.`,
145
- query,
146
- suggestedAction: "Fusionnez les deux contenus en une seule page maîtresse ou placez une balise canonique vers la plus performante.",
147
- });
148
- }
149
- }
150
- }
151
-
152
- return cannibalized;
153
- }
154
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * 🔎 LynxFlow SERP & Keyword Ranking Client
3
- *
4
- * Tracks keyword rankings on Google across multiple countries and languages,
5
- * discovers search volume, and analyzes AI Overview features.
6
- */
7
-
8
- export interface KeywordRankingItem {
9
- keyword: string;
10
- position: number;
11
- previousPosition?: number;
12
- searchVolume: number;
13
- difficulty: number; // 0 to 100
14
- url: string;
15
- hasAiOverview: boolean;
16
- country: string;
17
- }
18
-
19
- export interface SerpAnalysisResult {
20
- keyword: string;
21
- country: string;
22
- totalResults: number;
23
- rankings: { position: number; title: string; url: string; snippet: string }[];
24
- aiOverviewSnippet?: string;
25
- peopleAlsoAsk: string[];
26
- }
27
-
28
- export class SerpClient {
29
- private apiKey: string;
30
- private endpoint: string;
31
-
32
- constructor(apiKey: string, endpoint = "https://lynxintel.io/api/v1/serp") {
33
- this.apiKey = apiKey;
34
- this.endpoint = endpoint;
35
- }
36
-
37
- /**
38
- * Tracks a keyword ranking on Google for the client's domain.
39
- */
40
- async trackKeyword(keyword: string, country = "FR", domain = ""): Promise<KeywordRankingItem> {
41
- try {
42
- if (!this.apiKey || this.apiKey.startsWith("demo_")) {
43
- // Mocked realistic ranking
44
- return {
45
- keyword,
46
- position: 3,
47
- previousPosition: 5,
48
- searchVolume: 1800,
49
- difficulty: 34,
50
- url: `${domain}/solutions/${keyword.replace(/\s+/g, "-")}`,
51
- hasAiOverview: true,
52
- country,
53
- };
54
- }
55
-
56
- const res = await fetch(`${this.endpoint}/track`, {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
59
- body: JSON.stringify({ keyword, country, domain }),
60
- });
61
-
62
- if (!res.ok) throw new Error("SERP API error");
63
- return (await res.json()) as KeywordRankingItem;
64
- } catch {
65
- return {
66
- keyword,
67
- position: 1,
68
- searchVolume: 1200,
69
- difficulty: 25,
70
- url: domain,
71
- hasAiOverview: true,
72
- country,
73
- };
74
- }
75
- }
76
-
77
- /**
78
- * Discovers related keywords, search volume, and People Also Ask questions.
79
- */
80
- async getKeywordSuggestions(seedKeyword: string, country = "FR"): Promise<string[]> {
81
- return [
82
- `${seedKeyword} avis`,
83
- `${seedKeyword} tarif`,
84
- `meilleur ${seedKeyword} 2026`,
85
- `${seedKeyword} comparatif`,
86
- `${seedKeyword} alternative`,
87
- ];
88
- }
89
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * 📈 SERP Chronological History & Webhook Alert Engine (Pole 5)
3
- *
4
- * Tracks keyword rank history over time, detects ranking shifts,
5
- * and triggers automated alerts (Top 3, Top 10, rank drop).
6
- */
7
-
8
- export interface KeywordRankSnapshot {
9
- keyword: string;
10
- date: string; // ISO format: YYYY-MM-DD
11
- position: number;
12
- url: string;
13
- searchEngine: "google-mobile" | "google-desktop" | "bing";
14
- }
15
-
16
- export interface RankShiftAlert {
17
- keyword: string;
18
- previousPosition: number;
19
- newPosition: number;
20
- shiftType: "entered-top-3" | "entered-top-10" | "critical-drop" | "improved";
21
- message: string;
22
- timestamp: string;
23
- }
24
-
25
- export class SerpRankHistoryEngine {
26
- /**
27
- * Evaluates position shifts between two snapshots and returns alerts if thresholds are breached.
28
- */
29
- static analyzeRankShift(
30
- previousSnapshot: KeywordRankSnapshot,
31
- currentSnapshot: KeywordRankSnapshot,
32
- ): RankShiftAlert | null {
33
- const prev = previousSnapshot.position;
34
- const curr = currentSnapshot.position;
35
-
36
- if (prev === curr) return null;
37
-
38
- const diff = prev - curr; // Positive means gained positions, negative means dropped
39
-
40
- if (curr <= 3 && prev > 3) {
41
- return {
42
- keyword: currentSnapshot.keyword,
43
- previousPosition: prev,
44
- newPosition: curr,
45
- shiftType: "entered-top-3",
46
- message: `🚀 Le mot-clé "${currentSnapshot.keyword}" a pénétré le TOP 3 (#${curr}) !`,
47
- timestamp: new Date().toISOString(),
48
- };
49
- }
50
-
51
- if (curr <= 10 && prev > 10) {
52
- return {
53
- keyword: currentSnapshot.keyword,
54
- previousPosition: prev,
55
- newPosition: curr,
56
- shiftType: "entered-top-10",
57
- message: `🎯 Le mot-clé "${currentSnapshot.keyword}" est désormais en Première Page (#${curr}) !`,
58
- timestamp: new Date().toISOString(),
59
- };
60
- }
61
-
62
- if (diff <= -5) {
63
- return {
64
- keyword: currentSnapshot.keyword,
65
- previousPosition: prev,
66
- newPosition: curr,
67
- shiftType: "critical-drop",
68
- message: `⚠️ Chute de position détectée sur "${currentSnapshot.keyword}" (${prev} ➔ ${curr}).`,
69
- timestamp: new Date().toISOString(),
70
- };
71
- }
72
-
73
- return null;
74
- }
75
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * 🔍 Standalone SEO Site Auditor & Meta Quality Inspector
3
- *
4
- * Evaluates SEO health, title length, description CTR quality,
5
- * heading hierarchy, and AI direct-answer readiness on any page or string.
6
- */
7
-
8
- export interface SeoAuditReport {
9
- score: number; // 0 to 100
10
- status: "excellent" | "good" | "needs_improvement" | "poor";
11
- checks: {
12
- name: string;
13
- passed: boolean;
14
- message: string;
15
- weight: number;
16
- }[];
17
- suggestions: string[];
18
- }
19
-
20
- export class SiteAuditor {
21
- /**
22
- * Evaluates SEO quality for a given title, description, and h1.
23
- */
24
- static inspectMeta(params: {
25
- title: string;
26
- description: string;
27
- h1?: string;
28
- directAnswer?: string;
29
- brandName?: string;
30
- }): SeoAuditReport {
31
- const checks: SeoAuditReport["checks"] = [];
32
- const suggestions: string[] = [];
33
- let score = 0;
34
-
35
- // 1. Title Length Check (Optimal: 45 - 65 chars)
36
- const titleLen = (params.title || "").length;
37
- if (titleLen >= 40 && titleLen <= 70) {
38
- checks.push({ name: "Title Tag Length", passed: true, message: `Title length (${titleLen} chars) is optimal for Google SERP.`, weight: 25 });
39
- score += 25;
40
- } else {
41
- checks.push({ name: "Title Tag Length", passed: false, message: `Title length (${titleLen} chars) should be between 40 and 70 chars.`, weight: 25 });
42
- suggestions.push("Adjust title to be between 40 and 70 characters to avoid truncation in search results.");
43
- }
44
-
45
- // 2. Meta Description Length Check (Optimal: 120 - 160 chars)
46
- const descLen = (params.description || "").length;
47
- if (descLen >= 110 && descLen <= 170) {
48
- checks.push({ name: "Meta Description Length", passed: true, message: `Description length (${descLen} chars) is ideal.`, weight: 25 });
49
- score += 25;
50
- } else {
51
- checks.push({ name: "Meta Description Length", passed: false, message: `Description length (${descLen} chars) should be between 110 and 170 chars.`, weight: 25 });
52
- suggestions.push("Optimize meta description to 110-170 characters for higher click-through rates.");
53
- }
54
-
55
- // 3. H1 Heading Presence
56
- if (params.h1 && params.h1.trim().length > 5) {
57
- checks.push({ name: "H1 Tag Present", passed: true, message: "Primary H1 tag is clearly defined.", weight: 25 });
58
- score += 25;
59
- } else {
60
- checks.push({ name: "H1 Tag Present", passed: false, message: "Missing or too short H1 heading.", weight: 25 });
61
- suggestions.push("Ensure your page has a clear, keyword-rich H1 heading.");
62
- }
63
-
64
- // 4. AEO Direct Answer Check (For ChatGPT / Perplexity)
65
- if (params.directAnswer && params.directAnswer.trim().length > 30) {
66
- checks.push({ name: "AEO Direct Answer Ready", passed: true, message: "Direct-answer snippet is present for AI search engines.", weight: 25 });
67
- score += 25;
68
- } else {
69
- checks.push({ name: "AEO Direct Answer Ready", passed: false, message: "No concise direct answer block found for AI citation.", weight: 25 });
70
- suggestions.push("Add a 2-sentence summary block with structured facts for AI Overviews.");
71
- }
72
-
73
- const status: SeoAuditReport["status"] =
74
- score >= 90 ? "excellent" : score >= 75 ? "good" : score >= 50 ? "needs_improvement" : "poor";
75
-
76
- return {
77
- score,
78
- status,
79
- checks,
80
- suggestions,
81
- };
82
- }
83
- }