@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
package/src/rank-math-parity.ts
DELETED
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 👑 Rank Math Parity & Real-Time Content SEO Score Engine
|
|
3
|
-
*
|
|
4
|
-
* Implements the core features that made Rank Math famous (3M+ installs):
|
|
5
|
-
* 1. Real-Time 0-100 On-Page Content SEO Score (Checklist: Title, Slug, H2, Density, Words, Internal/External links)
|
|
6
|
-
* 2. 301/302/410 Redirection Manager & 404 Error Monitor
|
|
7
|
-
* 3. AI Image Alt Text Generator (Keyword-rich accessibility attributes)
|
|
8
|
-
* 4. Robots.txt Generator & AI Crawlers Rule Validator
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface OnPageCheckItem {
|
|
12
|
-
id: string;
|
|
13
|
-
category: "basic_seo" | "additional_seo" | "title_readability" | "content_readability";
|
|
14
|
-
label: string;
|
|
15
|
-
passed: boolean;
|
|
16
|
-
scoreImpact: number;
|
|
17
|
-
tip: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface OnPageSeoScoreResult {
|
|
21
|
-
score: number; // 0 to 100
|
|
22
|
-
grade: "poor" | "fair" | "good" | "great"; // < 50 = poor (red), 50-79 = fair (yellow), 80-89 = good (green), 90+ = great (bright green)
|
|
23
|
-
focusKeyword: string;
|
|
24
|
-
wordCount: number;
|
|
25
|
-
keywordDensityPercent: number;
|
|
26
|
-
checklist: OnPageCheckItem[];
|
|
27
|
-
criticalFixes: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface RedirectRule {
|
|
31
|
-
id: string;
|
|
32
|
-
sourcePath: string;
|
|
33
|
-
destinationUrl: string;
|
|
34
|
-
statusCode: 301 | 302 | 307 | 410 | 451;
|
|
35
|
-
isActive: boolean;
|
|
36
|
-
hitsCount: number;
|
|
37
|
-
createdAt: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class RankMathParityEngine {
|
|
41
|
-
/**
|
|
42
|
-
* 1. Calculates the exact 0-100 Real-Time On-Page Content SEO Score (Rank Math Benchmark).
|
|
43
|
-
*/
|
|
44
|
-
static calculateOnPageScore(opts: {
|
|
45
|
-
focusKeyword: string;
|
|
46
|
-
title: string;
|
|
47
|
-
description: string;
|
|
48
|
-
slug: string;
|
|
49
|
-
htmlOrMarkdownContent: string;
|
|
50
|
-
imagesCount?: number;
|
|
51
|
-
hasInternalLinks?: boolean;
|
|
52
|
-
hasExternalLinks?: boolean;
|
|
53
|
-
}): OnPageSeoScoreResult {
|
|
54
|
-
const kw = opts.focusKeyword.trim().toLowerCase();
|
|
55
|
-
const title = opts.title.toLowerCase();
|
|
56
|
-
const desc = opts.description.toLowerCase();
|
|
57
|
-
const slug = opts.slug.toLowerCase();
|
|
58
|
-
const content = opts.htmlOrMarkdownContent.toLowerCase();
|
|
59
|
-
|
|
60
|
-
const plainText = content.replace(/<[^>]+>/g, " ").replace(/[#*`_~]/g, " ");
|
|
61
|
-
const words = plainText.split(/\s+/).filter((w) => w.length > 1);
|
|
62
|
-
const wordCount = words.length;
|
|
63
|
-
|
|
64
|
-
// Count keyword occurrences
|
|
65
|
-
const kwRegex = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
66
|
-
const matches = content.match(kwRegex) || [];
|
|
67
|
-
const kwCount = matches.length;
|
|
68
|
-
const density = wordCount > 0 ? parseFloat(((kwCount / wordCount) * 100).toFixed(2)) : 0;
|
|
69
|
-
|
|
70
|
-
const checklist: OnPageCheckItem[] = [];
|
|
71
|
-
|
|
72
|
-
// --- Basic SEO ---
|
|
73
|
-
const inTitle = title.includes(kw);
|
|
74
|
-
checklist.push({
|
|
75
|
-
id: "kw_in_title",
|
|
76
|
-
category: "basic_seo",
|
|
77
|
-
label: `Mot-clé principal dans le titre SEO`,
|
|
78
|
-
passed: inTitle,
|
|
79
|
-
scoreImpact: 15,
|
|
80
|
-
tip: inTitle ? "Titre optimisé !" : `Ajoutez "${opts.focusKeyword}" dans votre balise Titre.`,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const inDesc = desc.includes(kw);
|
|
84
|
-
checklist.push({
|
|
85
|
-
id: "kw_in_desc",
|
|
86
|
-
category: "basic_seo",
|
|
87
|
-
label: `Mot-clé principal dans la Meta Description`,
|
|
88
|
-
passed: inDesc,
|
|
89
|
-
scoreImpact: 10,
|
|
90
|
-
tip: inDesc ? "Meta description optimisée !" : `Mentionnez "${opts.focusKeyword}" dans la description.`,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const inSlug = slug.includes(kw.replace(/\s+/g, "-")) || slug.includes(encodeURIComponent(kw));
|
|
94
|
-
checklist.push({
|
|
95
|
-
id: "kw_in_slug",
|
|
96
|
-
category: "basic_seo",
|
|
97
|
-
label: `Mot-clé principal dans l'URL / Slug`,
|
|
98
|
-
passed: inSlug,
|
|
99
|
-
scoreImpact: 10,
|
|
100
|
-
tip: inSlug ? "Slug court et ciblé !" : `Intégrez "${opts.focusKeyword}" dans l'URL.`,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const first10Percent = content.slice(0, Math.max(200, Math.floor(content.length * 0.15)));
|
|
104
|
-
const inBeginning = first10Percent.includes(kw);
|
|
105
|
-
checklist.push({
|
|
106
|
-
id: "kw_in_beginning",
|
|
107
|
-
category: "basic_seo",
|
|
108
|
-
label: `Mot-clé au début du contenu (premier paragraphe)`,
|
|
109
|
-
passed: inBeginning,
|
|
110
|
-
scoreImpact: 10,
|
|
111
|
-
tip: inBeginning ? "Accroche immédiate réussie !" : "Introduisez le mot-clé dans les 100 premiers mots.",
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const wordsPassed = wordCount >= 600;
|
|
115
|
-
checklist.push({
|
|
116
|
-
id: "content_length",
|
|
117
|
-
category: "basic_seo",
|
|
118
|
-
label: `Longueur du contenu suffisant (min. 600 mots)`,
|
|
119
|
-
passed: wordsPassed,
|
|
120
|
-
scoreImpact: 15,
|
|
121
|
-
tip: wordsPassed ? `${wordCount} mots (Parfait)` : `Votre contenu ne compte que ${wordCount} mots. Visez au moins 600 mots.`,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// --- Additional SEO ---
|
|
125
|
-
const inH2 = content.includes(`## `) || content.includes(`<h2`) || content.includes(`<h3`);
|
|
126
|
-
checklist.push({
|
|
127
|
-
id: "kw_in_subheadings",
|
|
128
|
-
category: "additional_seo",
|
|
129
|
-
label: `Structure en sous-titres H2/H3 présente`,
|
|
130
|
-
passed: inH2,
|
|
131
|
-
scoreImpact: 10,
|
|
132
|
-
tip: inH2 ? "Hiérarchie H2/H3 bien structurée !" : "Structurez votre page avec des sous-titres H2 et H3.",
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const densityPassed = density >= 0.5 && density <= 2.5;
|
|
136
|
-
checklist.push({
|
|
137
|
-
id: "kw_density",
|
|
138
|
-
category: "additional_seo",
|
|
139
|
-
label: `Densité de mot-clé équilibrée (0.5% - 2.5%)`,
|
|
140
|
-
passed: densityPassed,
|
|
141
|
-
scoreImpact: 10,
|
|
142
|
-
tip: densityPassed ? `Densité idéale (${density}%)` : `Densité actuelle : ${density}%. Évitez le bourrage ou le sous-dosage.`,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const internalPassed = opts.hasInternalLinks ?? true;
|
|
146
|
-
checklist.push({
|
|
147
|
-
id: "has_internal_links",
|
|
148
|
-
category: "additional_seo",
|
|
149
|
-
label: `Maillage interne vers d'autres pages du site`,
|
|
150
|
-
passed: internalPassed,
|
|
151
|
-
scoreImpact: 10,
|
|
152
|
-
tip: internalPassed ? "Maillage interne actif !" : "Ajoutez au moins 2 à 3 liens internes.",
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// --- Title & Content Readability ---
|
|
156
|
-
const titleLengthPassed = opts.title.length >= 40 && opts.title.length <= 65;
|
|
157
|
-
checklist.push({
|
|
158
|
-
id: "title_length",
|
|
159
|
-
category: "title_readability",
|
|
160
|
-
label: `Longueur idéale du titre (40-65 caractères)`,
|
|
161
|
-
passed: titleLengthPassed,
|
|
162
|
-
scoreImpact: 5,
|
|
163
|
-
tip: titleLengthPassed ? `${opts.title.length} car. (Taille parfaite)` : `Actuellement ${opts.title.length} car. Visez entre 40 et 65 caractères.`,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const hasImages = (opts.imagesCount || 0) > 0 || content.includes("<img") || content.includes("![");
|
|
167
|
-
checklist.push({
|
|
168
|
-
id: "has_media",
|
|
169
|
-
category: "content_readability",
|
|
170
|
-
label: `Présence d'images avec balises ALT`,
|
|
171
|
-
passed: hasImages,
|
|
172
|
-
scoreImpact: 5,
|
|
173
|
-
tip: hasImages ? "Images et visuels détectés !" : "Ajoutez au moins une image illustrative.",
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Calculate score
|
|
177
|
-
const totalScore = checklist.reduce((acc, item) => acc + (item.passed ? item.scoreImpact : 0), 0);
|
|
178
|
-
const score = Math.min(100, totalScore);
|
|
179
|
-
|
|
180
|
-
let grade: "poor" | "fair" | "good" | "great" = "poor";
|
|
181
|
-
if (score >= 90) grade = "great";
|
|
182
|
-
else if (score >= 80) grade = "good";
|
|
183
|
-
else if (score >= 50) grade = "fair";
|
|
184
|
-
|
|
185
|
-
const criticalFixes = checklist.filter((item) => !item.passed).map((item) => item.tip);
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
score,
|
|
189
|
-
grade,
|
|
190
|
-
focusKeyword: opts.focusKeyword,
|
|
191
|
-
wordCount,
|
|
192
|
-
keywordDensityPercent: density,
|
|
193
|
-
checklist,
|
|
194
|
-
criticalFixes,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 2. Generates SEO-rich Alt text for images based on surrounding context.
|
|
200
|
-
*/
|
|
201
|
-
static generateImageAltText(imageFilename: string, pageTopic: string, serviceName: string): string {
|
|
202
|
-
const cleanName = imageFilename
|
|
203
|
-
.replace(/\.[^/.]+$/, "")
|
|
204
|
-
.replace(/[-_]/g, " ")
|
|
205
|
-
.replace(/[^a-zA-Z0-9\s]/g, "");
|
|
206
|
-
|
|
207
|
-
return `${serviceName} : illustration de ${cleanName} pour ${pageTopic}`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 3. Generates optimal robots.txt rules including Search Engine & AI bot crawler directives.
|
|
212
|
-
*/
|
|
213
|
-
static generateRobotsTxt(domain: string, allowAiBots = true): string {
|
|
214
|
-
const sitemapUrl = `${domain.replace(/\/$/, "")}/sitemap.xml`;
|
|
215
|
-
|
|
216
|
-
return [
|
|
217
|
-
`User-agent: *`,
|
|
218
|
-
`Allow: /`,
|
|
219
|
-
`Disallow: /api/`,
|
|
220
|
-
`Disallow: /admin/`,
|
|
221
|
-
`Disallow: /checkout/`,
|
|
222
|
-
``,
|
|
223
|
-
`# 🤖 AI Assistants & Answer Engines (ChatGPT, Perplexity, Claude)`,
|
|
224
|
-
`User-agent: GPTBot`,
|
|
225
|
-
allowAiBots ? `Allow: /` : `Disallow: /`,
|
|
226
|
-
`User-agent: PerplexityBot`,
|
|
227
|
-
allowAiBots ? `Allow: /` : `Disallow: /`,
|
|
228
|
-
`User-agent: ClaudeBot`,
|
|
229
|
-
allowAiBots ? `Allow: /` : `Disallow: /`,
|
|
230
|
-
``,
|
|
231
|
-
`# 🗺️ Sitemaps`,
|
|
232
|
-
`Sitemap: ${sitemapUrl}`,
|
|
233
|
-
].join("\n");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🌐 LynxRateLimitedTranslator (Lightweight SDK Client)
|
|
3
|
-
*
|
|
4
|
-
* Provides fast, rate-limited translation for the SDK:
|
|
5
|
-
* 1. Uses local in-memory multilingual dictionary (0ms, 0€)
|
|
6
|
-
* 2. Connects to the LynxSEO Backend API for serverless / high-volume translations
|
|
7
|
-
* 3. Supports optional Google Cloud Translation API Key if configured
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { DICTIONARIES } from "./i18n-dictionary";
|
|
11
|
-
|
|
12
|
-
export interface TranslationClientConfig {
|
|
13
|
-
apiKey?: string;
|
|
14
|
-
backendEndpoint?: string; // e.g. https://lynxseo.studio/api/translate
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class LynxRateLimitedTranslator {
|
|
18
|
-
private static cache: Map<string, string> = new Map();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Translates an array of strings safely
|
|
22
|
-
*/
|
|
23
|
-
static async translateBatch(
|
|
24
|
-
contents: string[],
|
|
25
|
-
targetLang: string,
|
|
26
|
-
sourceLang = "en",
|
|
27
|
-
config: TranslationClientConfig = {}
|
|
28
|
-
): Promise<string[]> {
|
|
29
|
-
if (!contents || contents.length === 0) return [];
|
|
30
|
-
if (targetLang === sourceLang) return contents;
|
|
31
|
-
|
|
32
|
-
const unCachedIndices: number[] = [];
|
|
33
|
-
const results: string[] = new Array(contents.length);
|
|
34
|
-
|
|
35
|
-
contents.forEach((text, i) => {
|
|
36
|
-
const cacheKey = `${targetLang}:${text.trim()}`;
|
|
37
|
-
if (this.cache.has(cacheKey)) {
|
|
38
|
-
results[i] = this.cache.get(cacheKey)!;
|
|
39
|
-
} else {
|
|
40
|
-
unCachedIndices.push(i);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (unCachedIndices.length === 0) {
|
|
45
|
-
return results;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
49
|
-
let translatedChunks: string[] = [];
|
|
50
|
-
|
|
51
|
-
// If backend endpoint is configured, forward to LynxSEO backend
|
|
52
|
-
if (config.backendEndpoint) {
|
|
53
|
-
try {
|
|
54
|
-
const res = await fetch(`${config.backendEndpoint}/batch`, {
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers: { "Content-Type": "application/json" },
|
|
57
|
-
body: JSON.stringify({ contents: textsToTranslate, targetLang, sourceLang }),
|
|
58
|
-
});
|
|
59
|
-
if (res.ok) {
|
|
60
|
-
const json = await res.json();
|
|
61
|
-
translatedChunks = json.translations || textsToTranslate;
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
translatedChunks = textsToTranslate;
|
|
65
|
-
}
|
|
66
|
-
} else if (config.apiKey) {
|
|
67
|
-
// Direct Google Cloud API v2
|
|
68
|
-
try {
|
|
69
|
-
const url = `https://translation.googleapis.com/language/translate/v2?key=${config.apiKey}`;
|
|
70
|
-
const res = await fetch(url, {
|
|
71
|
-
method: "POST",
|
|
72
|
-
headers: { "Content-Type": "application/json" },
|
|
73
|
-
body: JSON.stringify({ q: textsToTranslate, target: targetLang, source: sourceLang, format: "text" }),
|
|
74
|
-
});
|
|
75
|
-
if (res.ok) {
|
|
76
|
-
const json = await res.json();
|
|
77
|
-
translatedChunks = json?.data?.translations?.map((t: any) => t.translatedText) || textsToTranslate;
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
translatedChunks = textsToTranslate;
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
// Fallback: Safe rate-limited public gateway
|
|
84
|
-
translatedChunks = [];
|
|
85
|
-
for (const text of textsToTranslate) {
|
|
86
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
87
|
-
try {
|
|
88
|
-
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
|
89
|
-
const res = await fetch(url);
|
|
90
|
-
if (res.ok) {
|
|
91
|
-
const json = await res.json();
|
|
92
|
-
translatedChunks.push(json?.[0]?.[0]?.[0] || text);
|
|
93
|
-
} else {
|
|
94
|
-
translatedChunks.push(text);
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
translatedChunks.push(text);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
103
|
-
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
104
|
-
results[originalIndex] = translatedText;
|
|
105
|
-
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return results;
|
|
109
|
-
}
|
|
110
|
-
}
|
package/src/real-reviews-sync.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🌟 Real Reviews & Star Ratings Ingestion Engine
|
|
3
|
-
*
|
|
4
|
-
* Official Compliance with Google Search Central Review Snippet Guidelines:
|
|
5
|
-
* 1. Requires reviews to be visibly rendered in the DOM (`generateVisibleReviewsHtmlBlock`).
|
|
6
|
-
* 2. Restricts AggregateRating to legitimate non-self-serving entities (Product, SoftwareApplication).
|
|
7
|
-
* 3. Pulls live authentic review records from Google Places API (New), Trustpilot API or DataForSEO.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface VerifiedReview {
|
|
11
|
-
id: string;
|
|
12
|
-
source: "google_places" | "trustpilot" | "g2" | "producthunt" | "verified_customer";
|
|
13
|
-
authorName: string;
|
|
14
|
-
ratingValue: number; // 1 to 5
|
|
15
|
-
reviewText: string;
|
|
16
|
-
datePublished: string; // YYYY-MM-DD
|
|
17
|
-
verifiedPurchase?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RealAggregateRating {
|
|
21
|
-
ratingValue: string; // e.g. "4.8"
|
|
22
|
-
reviewCount: string; // e.g. "142"
|
|
23
|
-
bestRating: "5";
|
|
24
|
-
worstRating: "1";
|
|
25
|
-
ratingDistribution: {
|
|
26
|
-
5: number;
|
|
27
|
-
4: number;
|
|
28
|
-
3: number;
|
|
29
|
-
2: number;
|
|
30
|
-
1: number;
|
|
31
|
-
};
|
|
32
|
-
sampleReviews: VerifiedReview[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class RealReviewsSyncEngine {
|
|
36
|
-
/**
|
|
37
|
-
* 1. Fetches authentic reviews directly from Google Places API (New).
|
|
38
|
-
* Endpoint: https://places.googleapis.com/v1/places/{PLACE_ID}?fields=rating,userRatingCount,reviews&key={API_KEY}
|
|
39
|
-
*/
|
|
40
|
-
static async fetchGooglePlacesReviews(opts: {
|
|
41
|
-
placeId: string;
|
|
42
|
-
googleApiKey: string;
|
|
43
|
-
languageCode?: string;
|
|
44
|
-
}): Promise<{ aggregate: RealAggregateRating | null; reviews: VerifiedReview[] }> {
|
|
45
|
-
try {
|
|
46
|
-
const url = `https://places.googleapis.com/v1/places/${encodeURIComponent(opts.placeId)}?fields=rating,userRatingCount,reviews&languageCode=${opts.languageCode || "fr"}&key=${encodeURIComponent(opts.googleApiKey)}`;
|
|
47
|
-
const res = await fetch(url);
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
throw new Error(`Google Places API returned ${res.status}`);
|
|
50
|
-
}
|
|
51
|
-
const data = await res.json() as any;
|
|
52
|
-
|
|
53
|
-
const reviews: VerifiedReview[] = (data.reviews || []).map((r: any, idx: number) => ({
|
|
54
|
-
id: `g_place_${idx}_${r.publishTime || Date.now()}`,
|
|
55
|
-
source: "google_places",
|
|
56
|
-
authorName: r.authorAttribution?.displayName || "Utilisateur Google",
|
|
57
|
-
ratingValue: r.rating || 5,
|
|
58
|
-
reviewText: r.text?.text || r.originalText?.text || "",
|
|
59
|
-
datePublished: r.publishTime ? r.publishTime.split("T")[0] : new Date().toISOString().split("T")[0],
|
|
60
|
-
verifiedPurchase: true,
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
const ratingVal = data.rating ? Number(data.rating).toFixed(1) : (reviews.length ? (reviews.reduce((a, b) => a + b.ratingValue, 0) / reviews.length).toFixed(1) : "5.0");
|
|
64
|
-
const reviewCnt = data.userRatingCount ? data.userRatingCount.toString() : reviews.length.toString();
|
|
65
|
-
|
|
66
|
-
const aggregate: RealAggregateRating = {
|
|
67
|
-
ratingValue: ratingVal,
|
|
68
|
-
reviewCount: reviewCnt,
|
|
69
|
-
bestRating: "5",
|
|
70
|
-
worstRating: "1",
|
|
71
|
-
ratingDistribution: {
|
|
72
|
-
5: Math.round(Number(reviewCnt) * 0.85),
|
|
73
|
-
4: Math.round(Number(reviewCnt) * 0.10),
|
|
74
|
-
3: Math.round(Number(reviewCnt) * 0.03),
|
|
75
|
-
2: Math.round(Number(reviewCnt) * 0.01),
|
|
76
|
-
1: Math.round(Number(reviewCnt) * 0.01),
|
|
77
|
-
},
|
|
78
|
-
sampleReviews: reviews,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return { aggregate, reviews };
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.warn("Could not fetch live Google Places reviews:", err);
|
|
84
|
-
return { aggregate: null, reviews: [] };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 2. Computes real AggregateRating & distribution from an array of verified reviews.
|
|
90
|
-
*/
|
|
91
|
-
static computeAggregateFromReviews(reviews: VerifiedReview[]): RealAggregateRating | null {
|
|
92
|
-
if (!reviews || reviews.length === 0) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
97
|
-
let sum = 0;
|
|
98
|
-
|
|
99
|
-
for (const r of reviews) {
|
|
100
|
-
const rounded = Math.min(5, Math.max(1, Math.round(r.ratingValue))) as 1 | 2 | 3 | 4 | 5;
|
|
101
|
-
distribution[rounded] = (distribution[rounded] || 0) + 1;
|
|
102
|
-
sum += r.ratingValue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const avg = (sum / reviews.length).toFixed(1);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
ratingValue: avg,
|
|
109
|
-
reviewCount: reviews.length.toString(),
|
|
110
|
-
bestRating: "5",
|
|
111
|
-
worstRating: "1",
|
|
112
|
-
ratingDistribution: distribution,
|
|
113
|
-
sampleReviews: reviews.slice(0, 5),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 3. Transforms raw DataForSEO or custom reviews into VerifiedReview format.
|
|
119
|
-
*/
|
|
120
|
-
static mapGoogleBusinessReviews(rawGoogleReviews: Array<{
|
|
121
|
-
author_title?: string;
|
|
122
|
-
rating?: { value?: number };
|
|
123
|
-
review_text?: string;
|
|
124
|
-
time_ago?: string;
|
|
125
|
-
timestamp?: string;
|
|
126
|
-
}>): VerifiedReview[] {
|
|
127
|
-
return (rawGoogleReviews || []).map((r, i) => {
|
|
128
|
-
const date = r.timestamp
|
|
129
|
-
? new Date(r.timestamp).toISOString().split("T")[0]
|
|
130
|
-
: new Date().toISOString().split("T")[0];
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
id: `g_rev_${i}_${Date.now()}`,
|
|
134
|
-
source: "google_places",
|
|
135
|
-
authorName: r.author_title || "Client Vérifié",
|
|
136
|
-
ratingValue: r.rating?.value || 5,
|
|
137
|
-
reviewText: r.review_text || "",
|
|
138
|
-
datePublished: date,
|
|
139
|
-
verifiedPurchase: true,
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 4. Converts verified reviews into Google Schema.org Review[] JSON-LD objects.
|
|
146
|
-
*/
|
|
147
|
-
static toSchemaOrgReviews(reviews: VerifiedReview[]): Record<string, unknown>[] {
|
|
148
|
-
return (reviews || []).map((r) => ({
|
|
149
|
-
"@type": "Review",
|
|
150
|
-
author: {
|
|
151
|
-
"@type": "Person",
|
|
152
|
-
name: r.authorName,
|
|
153
|
-
},
|
|
154
|
-
datePublished: r.datePublished,
|
|
155
|
-
reviewBody: r.reviewText,
|
|
156
|
-
reviewRating: {
|
|
157
|
-
"@type": "Rating",
|
|
158
|
-
ratingValue: r.ratingValue.toString(),
|
|
159
|
-
bestRating: "5",
|
|
160
|
-
worstRating: "1",
|
|
161
|
-
},
|
|
162
|
-
}));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 5. Generates the mandatory visible HTML review widget for the web page.
|
|
167
|
-
* Google Strict Requirement: Marked up reviews MUST be visible in the page content.
|
|
168
|
-
*/
|
|
169
|
-
static generateVisibleReviewsHtmlBlock(opts: {
|
|
170
|
-
aggregate: RealAggregateRating;
|
|
171
|
-
productName: string;
|
|
172
|
-
}): string {
|
|
173
|
-
const starStr = "★".repeat(Math.round(Number(opts.aggregate.ratingValue))) + "☆".repeat(5 - Math.round(Number(opts.aggregate.ratingValue)));
|
|
174
|
-
|
|
175
|
-
const reviewsHtml = opts.aggregate.sampleReviews
|
|
176
|
-
.map(
|
|
177
|
-
(r) => `
|
|
178
|
-
<div class="lynx-review-card" style="border: 1px solid #334155; border-radius: 8px; padding: 16px; margin-bottom: 12px; background: rgba(30, 41, 59, 0.5);">
|
|
179
|
-
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
180
|
-
<strong style="color: #f8fafc;">${r.authorName}</strong>
|
|
181
|
-
<span style="color: #fbbf24;">${"★".repeat(r.ratingValue)}${"☆".repeat(5 - r.ratingValue)}</span>
|
|
182
|
-
</div>
|
|
183
|
-
<p style="color: #cbd5e1; font-size: 0.95rem; margin: 0 0 8px 0;">"${r.reviewText}"</p>
|
|
184
|
-
<small style="color: #64748b;">Avis vérifié (${r.source}) • ${r.datePublished}</small>
|
|
185
|
-
</div>`
|
|
186
|
-
)
|
|
187
|
-
.join("");
|
|
188
|
-
|
|
189
|
-
return `
|
|
190
|
-
<section class="lynx-verified-reviews-section" style="margin: 32px 0;">
|
|
191
|
-
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px;">
|
|
192
|
-
<div>
|
|
193
|
-
<span style="font-size: 2.25rem; font-weight: 800; color: #f8fafc;">${opts.aggregate.ratingValue}</span>
|
|
194
|
-
<span style="color: #64748b; font-size: 1.1rem;"> / 5</span>
|
|
195
|
-
</div>
|
|
196
|
-
<div>
|
|
197
|
-
<div style="color: #fbbf24; font-size: 1.25rem;">${starStr}</div>
|
|
198
|
-
<p style="margin: 0; color: #94a3b8; font-size: 0.875rem;">Basé sur ${opts.aggregate.reviewCount} avis vérifiés pour ${opts.productName}</p>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
<div class="lynx-reviews-list">
|
|
202
|
-
${reviewsHtml}
|
|
203
|
-
</div>
|
|
204
|
-
</section>
|
|
205
|
-
`;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📰 RSS 2.0 & Atom Syndication Feed Generator (Inspiration: pages-seo/feed.xml.js)
|
|
3
|
-
*
|
|
4
|
-
* Automatically generates standards-compliant RSS 2.0 and Atom feeds for published
|
|
5
|
-
* programmatic landing pages and articles to accelerate syndication and indexing.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface FeedItem {
|
|
9
|
-
title: string;
|
|
10
|
-
url: string;
|
|
11
|
-
description: string;
|
|
12
|
-
publishedAt: string; // ISO date
|
|
13
|
-
author?: string;
|
|
14
|
-
category?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class RssSyndicationFeedGenerator {
|
|
18
|
-
/**
|
|
19
|
-
* Generates a valid RSS 2.0 XML string.
|
|
20
|
-
*/
|
|
21
|
-
static generateRss2(opts: {
|
|
22
|
-
siteTitle: string;
|
|
23
|
-
siteUrl: string;
|
|
24
|
-
description: string;
|
|
25
|
-
items: FeedItem[];
|
|
26
|
-
}): string {
|
|
27
|
-
const itemsXml = opts.items
|
|
28
|
-
.map(
|
|
29
|
-
(item) => `
|
|
30
|
-
<item>
|
|
31
|
-
<title><![CDATA[${item.title}]]></title>
|
|
32
|
-
<link>${item.url}</link>
|
|
33
|
-
<guid isPermaLink="true">${item.url}</guid>
|
|
34
|
-
<description><![CDATA[${item.description}]]></description>
|
|
35
|
-
<pubDate>${new Date(item.publishedAt).toUTCString()}</pubDate>
|
|
36
|
-
${item.category ? `<category>${item.category}</category>` : ""}
|
|
37
|
-
${item.author ? `<author>${item.author}</author>` : ""}
|
|
38
|
-
</item>
|
|
39
|
-
`.trim(),
|
|
40
|
-
)
|
|
41
|
-
.join("\n");
|
|
42
|
-
|
|
43
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
45
|
-
<channel>
|
|
46
|
-
<title><![CDATA[${opts.siteTitle}]]></title>
|
|
47
|
-
<link>${opts.siteUrl}</link>
|
|
48
|
-
<description><![CDATA[${opts.description}]]></description>
|
|
49
|
-
<atom:link href="${opts.siteUrl}/feed.xml" rel="self" type="application/rss+xml" />
|
|
50
|
-
<language>fr</language>
|
|
51
|
-
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
|
52
|
-
${itemsXml}
|
|
53
|
-
</channel>
|
|
54
|
-
</rss>`.trim();
|
|
55
|
-
}
|
|
56
|
-
}
|