@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.
- package/README.md +47 -93
- package/dist/ai-copilot-client.d.ts +2 -0
- package/dist/built-in-locations.d.ts +18 -0
- package/dist/content-ai-arsenal.d.ts +28 -0
- package/dist/existing-media-harvester.d.ts +33 -0
- package/dist/extended-schemas.d.ts +84 -0
- package/dist/features-knowledge-harvester.d.ts +56 -0
- package/dist/geo-citation-scorer.d.ts +34 -0
- package/dist/i18n-detector.d.ts +1 -1
- package/dist/index.d.ts +32 -7
- package/dist/index.js +538 -82
- package/dist/index.mjs +2028 -163
- package/dist/internal-pagerank-graph.d.ts +59 -0
- package/dist/knowledge-bank-builder.d.ts +91 -0
- package/dist/marketing-skills-engine.d.ts +74 -0
- package/dist/matrix-engine.d.ts +26 -0
- package/dist/power-words-psychology.d.ts +27 -15
- package/dist/public-manifest-engine.d.ts +58 -0
- package/dist/rag-knowledge-engine.d.ts +50 -0
- package/dist/rate-limited-translator.d.ts +19 -0
- package/dist/token-quota-manager.d.ts +4 -0
- package/package.json +11 -3
- package/TUTORIEL_INTEGRATION_SITE.md +0 -283
- package/connectors/cloudflare-worker/worker.js +0 -106
- package/connectors/laravel/LynxSeoController.php +0 -37
- package/connectors/wordpress/lynxseo-admin-app.js +0 -2611
- package/connectors/wordpress/lynxseo-connector.php +0 -1712
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/lynxflow-seo-engine-1.2.0.tgz +0 -0
- package/src/ad-intelligence-cro.ts +0 -140
- package/src/ai-bots-log-analyzer.ts +0 -127
- package/src/ai-copilot-client.ts +0 -99
- package/src/analytics-client.ts +0 -315
- package/src/auth-key.ts +0 -262
- package/src/backlinks-client.ts +0 -85
- package/src/brand-dna-calendar.ts +0 -120
- package/src/brand-icons.ts +0 -138
- package/src/built-in-locations.ts +0 -170
- package/src/content-ai-arsenal.ts +0 -268
- package/src/copy-frameworks-master.ts +0 -89
- package/src/cro-copywriting-engine.ts +0 -105
- package/src/crosslink-scorer.ts +0 -101
- package/src/embeddable-seo-widget.ts +0 -57
- package/src/engine.test.ts +0 -386
- package/src/engine.ts +0 -813
- package/src/existing-media-harvester.ts +0 -98
- package/src/extended-schemas.ts +0 -589
- package/src/features-knowledge-harvester.ts +0 -190
- package/src/free-public-tools.ts +0 -224
- package/src/geo-mesh-linking.ts +0 -93
- package/src/google-business-profile.ts +0 -280
- package/src/i18n-detector.ts +0 -98
- package/src/i18n-dictionary.ts +0 -422
- package/src/index.ts +0 -164
- package/src/indexnow-client.ts +0 -94
- package/src/instant-matrix-search.ts +0 -48
- package/src/isr-cache-manager.ts +0 -78
- package/src/keyword-permutator.ts +0 -118
- package/src/knowledge-bank-builder.ts +0 -299
- package/src/knowledge-graph-linker.ts +0 -81
- package/src/lago-token-meter.ts +0 -120
- package/src/legal-disclaimers.ts +0 -407
- package/src/llm-content-cleaner.ts +0 -64
- package/src/llm-prompt.ts +0 -323
- package/src/master-marketing-engine.ts +0 -314
- package/src/matrix-engine.ts +0 -1204
- package/src/mcp-seo-server.ts +0 -108
- package/src/ngram-density-analyzer.ts +0 -108
- package/src/og-image-generator.ts +0 -109
- package/src/power-words-psychology.ts +0 -369
- package/src/public-manifest-engine.ts +0 -109
- package/src/rag-knowledge-engine.ts +0 -163
- package/src/rank-math-parity.ts +0 -235
- package/src/rate-limited-translator.ts +0 -110
- package/src/real-reviews-sync.ts +0 -207
- package/src/rss-syndication-feed.ts +0 -56
- package/src/schema-builder.ts +0 -134
- package/src/seo-opportunities-decay.ts +0 -154
- package/src/serp-client.ts +0 -89
- package/src/serp-history-alerts.ts +0 -75
- package/src/site-auditor.ts +0 -83
- package/src/site-crawler.ts +0 -406
- package/src/slug-engine.ts +0 -183
- package/src/social-ads-seo.ts +0 -211
- package/src/social-growth-suite.ts +0 -310
- package/src/social-trend-seo.ts +0 -103
- package/src/social-video-seo.ts +0 -100
- package/src/team-rbac.ts +0 -61
- package/src/technical-rules-auditor.ts +0 -171
- package/src/token-quota-manager.ts +0 -91
- package/src/types.ts +0 -143
- package/src/ui-icons.ts +0 -134
- package/src/urlytics-engine.ts +0 -97
- package/src/video-youtube-analyzer.ts +0 -81
- package/src/yoast-parity.ts +0 -161
- package/tsconfig.json +0 -15
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📚 Features & Documentation Knowledge Harvester (Automated Content Extractor)
|
|
3
|
-
*
|
|
4
|
-
* Automatically crawls and ingests public documentation, feature pages, and WordPress sitemaps.
|
|
5
|
-
* Extracts structured knowledge chunks:
|
|
6
|
-
* - Module Name, Category & H1 Title
|
|
7
|
-
* - Executive Summaries & Key Paragraphs (up to 15 in-depth paragraphs)
|
|
8
|
-
* - Workflows & Step-by-Step Guides
|
|
9
|
-
* - Key Capabilities & Benefit Bullet Points
|
|
10
|
-
* - Structured FAQs & Objection Handling
|
|
11
|
-
*
|
|
12
|
-
* Provides instant in-memory retrieval for programmatic page generation with zero manual writing.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface ExtractedFeatureKnowledge {
|
|
16
|
-
slug: string;
|
|
17
|
-
url: string;
|
|
18
|
-
title: string;
|
|
19
|
-
headline?: string;
|
|
20
|
-
category: string;
|
|
21
|
-
executiveSummary: string;
|
|
22
|
-
paragraphs: string[]; // Up to 15 clean thematic paragraphs
|
|
23
|
-
keyBenefits: string[];
|
|
24
|
-
workflowSteps: { step: number; title: string; description: string }[];
|
|
25
|
-
faqs: { question: string; answer: string }[];
|
|
26
|
-
rawTextLength: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class FeaturesKnowledgeHarvester {
|
|
30
|
-
private static knowledgeStore: Map<string, ExtractedFeatureKnowledge> = new Map();
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Extract knowledge from raw HTML or Markdown content of a public page
|
|
34
|
-
*/
|
|
35
|
-
static extractFromHtml(url: string, html: string, slug?: string): ExtractedFeatureKnowledge {
|
|
36
|
-
const pageSlug = slug || url.split("/").filter(Boolean).pop() || "feature";
|
|
37
|
-
|
|
38
|
-
// Extract Title (H1 or <title>)
|
|
39
|
-
const titleMatch = html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i) || html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
40
|
-
const title = titleMatch ? titleMatch[1].replace(/<[^>]+>/g, "").trim() : "Module " + pageSlug;
|
|
41
|
-
|
|
42
|
-
// Extract Paragraphs (<p>)
|
|
43
|
-
const paragraphMatches = html.matchAll(/<p[^>]*>([\s\S]*?)<\/p>/gi);
|
|
44
|
-
const paragraphs: string[] = [];
|
|
45
|
-
for (const match of paragraphMatches) {
|
|
46
|
-
const clean = match[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
47
|
-
if (clean.length > 40 && !clean.includes("Cookie") && !clean.includes("Copyright")) {
|
|
48
|
-
paragraphs.push(clean);
|
|
49
|
-
if (paragraphs.length >= 15) break; // Keep the top 15 rich paragraphs
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Extract List Items (<li>) as Key Benefits
|
|
54
|
-
const liMatches = html.matchAll(/<li[^>]*>([\s\S]*?)<\/li>/gi);
|
|
55
|
-
const keyBenefits: string[] = [];
|
|
56
|
-
for (const match of liMatches) {
|
|
57
|
-
const clean = match[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
58
|
-
if (clean.length > 20 && clean.length < 200) {
|
|
59
|
-
keyBenefits.push(clean);
|
|
60
|
-
if (keyBenefits.length >= 8) break;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Extract FAQs (H3/H4 questions with following text or <details>/<summary>)
|
|
65
|
-
const faqs: { question: string; answer: string }[] = [];
|
|
66
|
-
const faqMatches = html.matchAll(/<summary[^>]*>([\s\S]*?)<\/summary>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/gi);
|
|
67
|
-
for (const match of faqMatches) {
|
|
68
|
-
const question = match[1].replace(/<[^>]+>/g, "").trim();
|
|
69
|
-
const answer = match[2].replace(/<[^>]+>/g, "").trim();
|
|
70
|
-
if (question && answer) {
|
|
71
|
-
faqs.push({ question, answer });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Default workflow steps if not parsed
|
|
76
|
-
const workflowSteps = [
|
|
77
|
-
{ step: 1, title: "Configuration Automatique", description: `Activation du module ${title} en 1 clic.` },
|
|
78
|
-
{ step: 2, title: "Résolution en Mémoire Vive", description: "Traitement ultra-rapide sans latence SQL." },
|
|
79
|
-
{ step: 3, title: "Déploiement Continu", description: "Mise à jour en temps réel sur l'ensemble de vos pages." },
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
const executiveSummary = paragraphs.slice(0, 2).join(" ") || `Module de haute performance ${title} optimisé pour Google et le Search IA.`;
|
|
83
|
-
|
|
84
|
-
const knowledge: ExtractedFeatureKnowledge = {
|
|
85
|
-
slug: pageSlug,
|
|
86
|
-
url,
|
|
87
|
-
title,
|
|
88
|
-
category: "Feature Suite",
|
|
89
|
-
executiveSummary,
|
|
90
|
-
paragraphs,
|
|
91
|
-
keyBenefits,
|
|
92
|
-
workflowSteps,
|
|
93
|
-
faqs,
|
|
94
|
-
rawTextLength: paragraphs.join(" ").length,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
this.knowledgeStore.set(pageSlug, knowledge);
|
|
98
|
-
return knowledge;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Batch ingest multiple pages from a sitemap or list of URLs
|
|
103
|
-
*/
|
|
104
|
-
static async ingestPagesFromSitemap(sitemapUrl: string): Promise<number> {
|
|
105
|
-
try {
|
|
106
|
-
const res = await fetch(sitemapUrl);
|
|
107
|
-
if (!res.ok) return 0;
|
|
108
|
-
const xml = await res.text();
|
|
109
|
-
const urls = [...xml.matchAll(/<loc>([\s\S]*?)<\/loc>/gi)].map((m) => m[1].trim());
|
|
110
|
-
|
|
111
|
-
let count = 0;
|
|
112
|
-
for (const u of urls.slice(0, 50)) {
|
|
113
|
-
try {
|
|
114
|
-
const pageRes = await fetch(u);
|
|
115
|
-
if (pageRes.ok) {
|
|
116
|
-
const html = await pageRes.text();
|
|
117
|
-
this.extractFromHtml(u, html);
|
|
118
|
-
count++;
|
|
119
|
-
}
|
|
120
|
-
} catch {
|
|
121
|
-
// ignore network timeout
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return count;
|
|
125
|
-
} catch {
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Retrieve extracted knowledge for any module by slug
|
|
132
|
-
*/
|
|
133
|
-
static getKnowledge(slug: string): ExtractedFeatureKnowledge | undefined {
|
|
134
|
-
return this.knowledgeStore.get(slug);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get all registered module knowledge chunks
|
|
139
|
-
*/
|
|
140
|
-
static getAllKnowledge(): ExtractedFeatureKnowledge[] {
|
|
141
|
-
return Array.from(this.knowledgeStore.values());
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Generate an ultra-complete, 1000-word programmatic page using harvested knowledge
|
|
146
|
-
*/
|
|
147
|
-
static buildEnrichedPage(slug: string, locationOrSector: string): string {
|
|
148
|
-
const k = this.getKnowledge(slug);
|
|
149
|
-
if (!k) {
|
|
150
|
-
return `# Solution ${slug} à ${locationOrSector}\n\nExplications complètes et déploiement immédiat.`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return `
|
|
154
|
-
# ${k.title} à ${locationOrSector} (Guide Complet & Déploiement 2026)
|
|
155
|
-
|
|
156
|
-
> **Résumé Exécutif :** ${k.executiveSummary}
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## 📖 Analyse Approfondie & Fonctionnement
|
|
161
|
-
|
|
162
|
-
${k.paragraphs.slice(0, 6).join("\n\n")}
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## 💎 Fonctionnalités Clés & Avantages Concrets :
|
|
167
|
-
|
|
168
|
-
${k.keyBenefits.map((b) => `* **${b}**`).join("\n")}
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## 🛠️ Guide de Mise en Œuvre en 3 Étapes :
|
|
173
|
-
|
|
174
|
-
${k.workflowSteps.map((s) => `${s.step}. **${s.title}** : ${s.description}`).join("\n")}
|
|
175
|
-
|
|
176
|
-
---
|
|
177
|
-
|
|
178
|
-
${k.paragraphs.slice(6, 12).join("\n\n")}
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## ❓ Questions Fréquentes & Réponses Officielles :
|
|
183
|
-
|
|
184
|
-
${k.faqs.length > 0
|
|
185
|
-
? k.faqs.map((f) => `### ${f.question}\n${f.answer}`).join("\n\n")
|
|
186
|
-
: `### Comment activer ${k.title} à ${locationOrSector} ?\nL'activation est instantanée en 1 clic sans compétences techniques.`
|
|
187
|
-
}
|
|
188
|
-
`.trim();
|
|
189
|
-
}
|
|
190
|
-
}
|
package/src/free-public-tools.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🛠️ Free Public Interactive Tools & Lead Magnets Suite
|
|
3
|
-
* Inspired by Ahrefs, Semrush, Moz, and WordStream free tool ecosystems.
|
|
4
|
-
*
|
|
5
|
-
* Provides 6 zero-dependency interactive calculators and simulators that can be embedded
|
|
6
|
-
* on public programmatic pages to maximize dwell time, engagement, backlink acquisition, and user conversion.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { cleanSeoSlug } from "./slug-engine";
|
|
10
|
-
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
11
|
-
|
|
12
|
-
export interface SerpSimulatorResult {
|
|
13
|
-
title: string;
|
|
14
|
-
truncatedTitle: string;
|
|
15
|
-
titlePixelWidth: number;
|
|
16
|
-
isTitleTruncated: boolean;
|
|
17
|
-
description: string;
|
|
18
|
-
truncatedDescription: string;
|
|
19
|
-
isDescriptionTruncated: boolean;
|
|
20
|
-
urlPreview: string;
|
|
21
|
-
estimatedCtrScore: number; // 0 to 100
|
|
22
|
-
recommendations: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RoiCalculatorResult {
|
|
26
|
-
hoursSavedWeekly: number;
|
|
27
|
-
hoursSavedMonthly: number;
|
|
28
|
-
monthlyCostSavings: number;
|
|
29
|
-
annualCostSavings: number;
|
|
30
|
-
roiPercentage: number;
|
|
31
|
-
breakEvenDays: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface DensityAnalysisResult {
|
|
35
|
-
totalWords: number;
|
|
36
|
-
characterCount: number;
|
|
37
|
-
readingTimeMinutes: number;
|
|
38
|
-
topKeywords: { word: string; count: number; densityPercent: number }[];
|
|
39
|
-
seoReadabilityScore: number; // 0 to 100
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class FreePublicToolsEngine {
|
|
43
|
-
/**
|
|
44
|
-
* 1. Google SERP Preview & CTR Optimizer Simulator (Ahrefs / Moz Parity)
|
|
45
|
-
*/
|
|
46
|
-
static simulateSerpSnippet(input: {
|
|
47
|
-
title: string;
|
|
48
|
-
description: string;
|
|
49
|
-
url: string;
|
|
50
|
-
targetKeyword?: string;
|
|
51
|
-
}): SerpSimulatorResult {
|
|
52
|
-
const title = (input.title || "").trim();
|
|
53
|
-
const description = (input.description || "").trim();
|
|
54
|
-
const url = (input.url || "https://example.com").trim();
|
|
55
|
-
const kw = (input.targetKeyword || "").toLowerCase().trim();
|
|
56
|
-
|
|
57
|
-
// Average pixel width estimation (approx 10px per char on Arial 20px)
|
|
58
|
-
const titlePixelWidth = title.length * 9.5;
|
|
59
|
-
const maxTitlePixels = 580; // Google Desktop breakpoint
|
|
60
|
-
const maxDescChars = 158;
|
|
61
|
-
|
|
62
|
-
const isTitleTruncated = titlePixelWidth > maxTitlePixels;
|
|
63
|
-
const truncatedTitle = isTitleTruncated ? title.slice(0, 56) + "..." : title;
|
|
64
|
-
|
|
65
|
-
const isDescriptionTruncated = description.length > maxDescChars;
|
|
66
|
-
const truncatedDescription = isDescriptionTruncated ? description.slice(0, 155) + "..." : description;
|
|
67
|
-
|
|
68
|
-
let ctrScore = 50;
|
|
69
|
-
const recommendations: string[] = [];
|
|
70
|
-
|
|
71
|
-
// Title checks
|
|
72
|
-
if (title.length >= 40 && title.length <= 60) {
|
|
73
|
-
ctrScore += 20;
|
|
74
|
-
} else if (title.length < 30) {
|
|
75
|
-
recommendations.push("Le titre est trop court (< 30 car.). Ajoutez des précisions pour booster le clic.");
|
|
76
|
-
} else if (isTitleTruncated) {
|
|
77
|
-
ctrScore -= 10;
|
|
78
|
-
recommendations.push("Le titre dépasse 580px et sera tronqué sur Google Desktop.");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Keyword presence
|
|
82
|
-
if (kw && title.toLowerCase().includes(kw)) {
|
|
83
|
-
ctrScore += 15;
|
|
84
|
-
} else if (kw) {
|
|
85
|
-
recommendations.push(`Le mot-clé principal "${kw}" est absent du titre.`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Description checks
|
|
89
|
-
if (description.length >= 120 && description.length <= 158) {
|
|
90
|
-
ctrScore += 15;
|
|
91
|
-
} else if (description.length < 80) {
|
|
92
|
-
recommendations.push("La méta-description est trop courte (< 80 car.).");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
title,
|
|
97
|
-
truncatedTitle,
|
|
98
|
-
titlePixelWidth: Math.round(titlePixelWidth),
|
|
99
|
-
isTitleTruncated,
|
|
100
|
-
description,
|
|
101
|
-
truncatedDescription,
|
|
102
|
-
isDescriptionTruncated,
|
|
103
|
-
urlPreview: url,
|
|
104
|
-
estimatedCtrScore: Math.min(Math.max(ctrScore, 10), 100),
|
|
105
|
-
recommendations,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 2. Free ROI & Time-Saved Automation Calculator
|
|
111
|
-
*/
|
|
112
|
-
static calculateRoi(input: {
|
|
113
|
-
hoursSpentPerWeek: number;
|
|
114
|
-
hourlyRateOrSalary: number;
|
|
115
|
-
teamMembersCount?: number;
|
|
116
|
-
softwareMonthlyPrice?: number;
|
|
117
|
-
}): RoiCalculatorResult {
|
|
118
|
-
const hours = Math.max(input.hoursSpentPerWeek || 5, 1);
|
|
119
|
-
const rate = Math.max(input.hourlyRateOrSalary || 35, 1);
|
|
120
|
-
const team = Math.max(input.teamMembersCount || 1, 1);
|
|
121
|
-
const softwareCost = input.softwareMonthlyPrice ?? 49;
|
|
122
|
-
|
|
123
|
-
// Assuming automation saves ~75% of manual operational time
|
|
124
|
-
const hoursSavedWeekly = Math.round(hours * 0.75 * team * 10) / 10;
|
|
125
|
-
const hoursSavedMonthly = Math.round(hoursSavedWeekly * 4.33);
|
|
126
|
-
|
|
127
|
-
const monthlyGrossSavings = hoursSavedMonthly * rate;
|
|
128
|
-
const monthlyNetSavings = Math.max(monthlyGrossSavings - softwareCost, 0);
|
|
129
|
-
const annualCostSavings = monthlyNetSavings * 12;
|
|
130
|
-
|
|
131
|
-
const roiPercentage = softwareCost > 0 ? Math.round((monthlyNetSavings / softwareCost) * 100) : 1000;
|
|
132
|
-
const breakEvenDays = monthlyGrossSavings > 0 ? Math.max(Math.round((softwareCost / (monthlyGrossSavings / 30))), 1) : 30;
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
hoursSavedWeekly,
|
|
136
|
-
hoursSavedMonthly,
|
|
137
|
-
monthlyCostSavings: Math.round(monthlyNetSavings),
|
|
138
|
-
annualCostSavings: Math.round(annualCostSavings),
|
|
139
|
-
roiPercentage,
|
|
140
|
-
breakEvenDays,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 3. Instant Keyword Density & Readability Analyzer
|
|
146
|
-
*/
|
|
147
|
-
static analyzeTextDensity(text: string, language: string = "fr"): DensityAnalysisResult {
|
|
148
|
-
if (!text || typeof text !== "string") {
|
|
149
|
-
return { totalWords: 0, characterCount: 0, readingTimeMinutes: 0, topKeywords: [], seoReadabilityScore: 0 };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const clean = text.toLowerCase().replace(/[^a-z0-9à-ÿ\s]/g, " ");
|
|
153
|
-
const words = clean.split(/\s+/).filter((w) => w.length > 2);
|
|
154
|
-
const totalWords = words.length;
|
|
155
|
-
const characterCount = text.length;
|
|
156
|
-
const readingTimeMinutes = Math.max(Math.round((totalWords / 200) * 10) / 10, 0.5);
|
|
157
|
-
|
|
158
|
-
const stopWords = new Set(["pour", "dans", "avec", "sur", "les", "des", "une", "que", "qui", "est", "par", "the", "and", "for", "with", "this", "that"]);
|
|
159
|
-
const frequencyMap = new Map<string, number>();
|
|
160
|
-
|
|
161
|
-
for (const w of words) {
|
|
162
|
-
if (!stopWords.has(w)) {
|
|
163
|
-
frequencyMap.set(w, (frequencyMap.get(w) || 0) + 1);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const sorted = Array.from(frequencyMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
168
|
-
const topKeywords = sorted.slice(0, 8).map(([word, count]) => ({
|
|
169
|
-
word,
|
|
170
|
-
count,
|
|
171
|
-
densityPercent: totalWords > 0 ? Math.round((count / totalWords) * 1000) / 10 : 0,
|
|
172
|
-
}));
|
|
173
|
-
|
|
174
|
-
let seoReadabilityScore = 70;
|
|
175
|
-
if (totalWords >= 600) seoReadabilityScore += 20;
|
|
176
|
-
else if (totalWords < 300) seoReadabilityScore -= 25;
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
totalWords,
|
|
180
|
-
characterCount,
|
|
181
|
-
readingTimeMinutes,
|
|
182
|
-
topKeywords,
|
|
183
|
-
seoReadabilityScore: Math.min(Math.max(seoReadabilityScore, 10), 100),
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 4. Free SEO Slug Cleanser Helper
|
|
189
|
-
*/
|
|
190
|
-
static cleanSlugTool(input: string, lang: string = "fr"): { original: string; cleanSlug: string; charSavings: number } {
|
|
191
|
-
const clean = cleanSeoSlug(input, { language: lang });
|
|
192
|
-
return {
|
|
193
|
-
original: input,
|
|
194
|
-
cleanSlug: clean,
|
|
195
|
-
charSavings: Math.max(input.length - clean.length, 0),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* 5. Free Schema.org Generator Helper
|
|
201
|
-
*/
|
|
202
|
-
static generateSampleSchema(type: "localBusiness" | "softwareApp" | "faq", name: string, url: string): Record<string, unknown> {
|
|
203
|
-
if (type === "localBusiness") {
|
|
204
|
-
return ExtendedSchemaGraphBuilder.buildLocalBusiness({
|
|
205
|
-
name,
|
|
206
|
-
url,
|
|
207
|
-
city: "Paris",
|
|
208
|
-
country: "France",
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
if (type === "faq") {
|
|
212
|
-
return ExtendedSchemaGraphBuilder.buildFAQPage([
|
|
213
|
-
{ question: `Comment fonctionne ${name} ?`, answer: `${name} automatise vos opérations en quelques clics.` },
|
|
214
|
-
{ question: `Quel est le tarif de ${name} ?`, answer: `Un essai gratuit de 14 jours est disponible sans carte bancaire.` },
|
|
215
|
-
]);
|
|
216
|
-
}
|
|
217
|
-
return ExtendedSchemaGraphBuilder.buildSoftwareApplication({
|
|
218
|
-
name,
|
|
219
|
-
description: `Solution logicielle professionnelle ${name}`,
|
|
220
|
-
url,
|
|
221
|
-
priceMonthly: 29,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
package/src/geo-mesh-linking.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🗺️ Geographic Mesh Linking Engine
|
|
3
|
-
*
|
|
4
|
-
* Implements geodesic distance calculation (Haversine formula) and
|
|
5
|
-
* administrative zone clustering to connect neighboring cities and propagate PageRank.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface GeoCoordinate {
|
|
9
|
-
latitude: number;
|
|
10
|
-
longitude: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface MeshCity {
|
|
14
|
-
slug: string;
|
|
15
|
-
name: string;
|
|
16
|
-
departmentCode?: string;
|
|
17
|
-
region?: string;
|
|
18
|
-
country: string;
|
|
19
|
-
coordinates?: GeoCoordinate;
|
|
20
|
-
population?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class GeoMeshLinkingEngine {
|
|
24
|
-
/**
|
|
25
|
-
* Computes the great-circle distance between two points in kilometers (Haversine formula).
|
|
26
|
-
*/
|
|
27
|
-
static computeDistanceKm(coord1: GeoCoordinate, coord2: GeoCoordinate): number {
|
|
28
|
-
const R = 6371; // Earth radius in KM
|
|
29
|
-
const dLat = this.deg2rad(coord2.latitude - coord1.latitude);
|
|
30
|
-
const dLon = this.deg2rad(coord2.longitude - coord1.longitude);
|
|
31
|
-
|
|
32
|
-
const a =
|
|
33
|
-
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
34
|
-
Math.cos(this.deg2rad(coord1.latitude)) *
|
|
35
|
-
Math.cos(this.deg2rad(coord2.latitude)) *
|
|
36
|
-
Math.sin(dLon / 2) *
|
|
37
|
-
Math.sin(dLon / 2);
|
|
38
|
-
|
|
39
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
40
|
-
return R * c;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private static deg2rad(deg: number): number {
|
|
44
|
-
return deg * (Math.PI / 180);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Finds the N closest neighbor cities within the same country / department / distance.
|
|
49
|
-
*/
|
|
50
|
-
static findClosestNeighbors(
|
|
51
|
-
targetCity: MeshCity,
|
|
52
|
-
allCities: MeshCity[],
|
|
53
|
-
maxNeighbors = 4,
|
|
54
|
-
maxDistanceKm = 50,
|
|
55
|
-
): { city: MeshCity; distanceKm?: number }[] {
|
|
56
|
-
const candidateCities = allCities.filter(
|
|
57
|
-
(c) => c.slug !== targetCity.slug && c.country === targetCity.country,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
// If coordinates are available, calculate exact geodesic proximity
|
|
61
|
-
if (targetCity.coordinates) {
|
|
62
|
-
const withDistance = candidateCities
|
|
63
|
-
.filter((c) => Boolean(c.coordinates))
|
|
64
|
-
.map((c) => ({
|
|
65
|
-
city: c,
|
|
66
|
-
distanceKm: this.computeDistanceKm(targetCity.coordinates!, c.coordinates!),
|
|
67
|
-
}))
|
|
68
|
-
.filter((item) => item.distanceKm <= maxDistanceKm)
|
|
69
|
-
.sort((a, b) => a.distanceKm - b.distanceKm);
|
|
70
|
-
|
|
71
|
-
if (withDistance.length > 0) {
|
|
72
|
-
return withDistance.slice(0, maxNeighbors);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Fallback: match by same department or region
|
|
77
|
-
const sameDepartment = candidateCities.filter(
|
|
78
|
-
(c) =>
|
|
79
|
-
targetCity.departmentCode &&
|
|
80
|
-
c.departmentCode === targetCity.departmentCode,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
if (sameDepartment.length > 0) {
|
|
84
|
-
return sameDepartment.slice(0, maxNeighbors).map((c) => ({ city: c }));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const sameRegion = candidateCities.filter(
|
|
88
|
-
(c) => targetCity.region && c.region === targetCity.region,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
return sameRegion.slice(0, maxNeighbors).map((c) => ({ city: c }));
|
|
92
|
-
}
|
|
93
|
-
}
|