@lynxflow/seo-engine 1.8.29 → 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 -2618
  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,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
- }
@@ -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
- }