@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,101 +0,0 @@
1
- /**
2
- * 🕸️ Contextual Cross-Linking & Semantic Affinity Engine (Inspiration: seedrank/crosslinks.py)
3
- *
4
- * Computes forward and backward internal link affinity scores based on:
5
- * Score = (TopicOverlap * 2 + KeywordOverlap) + VolumeBonus + ContentTypeBonus + RecencyBonus
6
- */
7
-
8
- export interface ArticleOrPageNode {
9
- slug: string;
10
- title: string;
11
- url: string;
12
- targetKeywords: string[];
13
- topics: string[];
14
- contentType?: "landing-page" | "blog-post" | "comparison" | "use-case";
15
- searchVolumeSum?: number;
16
- publishedAt?: string; // ISO date
17
- }
18
-
19
- export interface CrosslinkSuggestion {
20
- targetNode: ArticleOrPageNode;
21
- score: number;
22
- keywordOverlapCount: number;
23
- topicOverlapCount: number;
24
- anchorTextSuggestion: string;
25
- }
26
-
27
- export class CrosslinkScorerEngine {
28
- /**
29
- * Computes semantic crosslink affinity between a source page and candidate pages.
30
- */
31
- static computeCrosslinkScore(
32
- source: ArticleOrPageNode,
33
- candidate: ArticleOrPageNode,
34
- ): CrosslinkSuggestion | null {
35
- if (source.slug === candidate.slug) return null;
36
-
37
- const sourceKws = new Set(source.targetKeywords.map((k) => k.toLowerCase()));
38
- const candKws = new Set(candidate.targetKeywords.map((k) => k.toLowerCase()));
39
- const sourceTopics = new Set(source.topics.map((t) => t.toLowerCase()));
40
- const candTopics = new Set(candidate.topics.map((t) => t.toLowerCase()));
41
-
42
- const kwOverlap = Array.from(sourceKws).filter((k) => candKws.has(k));
43
- const topicOverlap = Array.from(sourceTopics).filter((t) => candTopics.has(t));
44
-
45
- const baseScore = topicOverlap.length * 2 + kwOverlap.length;
46
- if (baseScore === 0) return null;
47
-
48
- // Search volume bonus (up to +1.0)
49
- let volumeBonus = 0.0;
50
- if (candidate.searchVolumeSum) {
51
- volumeBonus = Math.min(candidate.searchVolumeSum / 2000, 1.0);
52
- }
53
-
54
- // Content type affinity (+0.5 if different types create funnel synergy)
55
- let contentTypeBonus = 0.0;
56
- if (source.contentType && candidate.contentType && source.contentType !== candidate.contentType) {
57
- contentTypeBonus = 0.5; // Blog post linking to Landing page is high-value
58
- }
59
-
60
- // Recency bonus
61
- let recencyBonus = 0.0;
62
- if (candidate.publishedAt) {
63
- const daysAgo = (Date.now() - new Date(candidate.publishedAt).getTime()) / (1000 * 3600 * 24);
64
- if (daysAgo < 30) recencyBonus = 0.5;
65
- else if (daysAgo < 90) recencyBonus = 0.25;
66
- }
67
-
68
- const totalScore = baseScore + volumeBonus + contentTypeBonus + recencyBonus;
69
-
70
- const anchorText =
71
- kwOverlap.length > 0 ? candidate.targetKeywords[0] : candidate.title;
72
-
73
- return {
74
- targetNode: candidate,
75
- score: Math.round(totalScore * 100) / 100,
76
- keywordOverlapCount: kwOverlap.length,
77
- topicOverlapCount: topicOverlap.length,
78
- anchorTextSuggestion: anchorText,
79
- };
80
- }
81
-
82
- /**
83
- * Finds the top N most relevant internal crosslinks for a given page.
84
- */
85
- static findBestCrosslinks(
86
- source: ArticleOrPageNode,
87
- allNodes: ArticleOrPageNode[],
88
- maxLinks = 5,
89
- ): CrosslinkSuggestion[] {
90
- const suggestions: CrosslinkSuggestion[] = [];
91
-
92
- for (const cand of allNodes) {
93
- const scoreResult = this.computeCrosslinkScore(source, cand);
94
- if (scoreResult) {
95
- suggestions.push(scoreResult);
96
- }
97
- }
98
-
99
- return suggestions.sort((a, b) => b.score - a.score).slice(0, maxLinks);
100
- }
101
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * 🧩 Embeddable Client SEO & Discovery Widget (Inspiration: pages-seo/widget.js.js)
3
- *
4
- * Provides a lightweight (< 5KB) vanilla JavaScript snippet generator that allows external
5
- * client sites to embed interactive directory navigation or related programmatic links.
6
- */
7
-
8
- export class EmbeddableSeoWidgetGenerator {
9
- /**
10
- * Generates the client-side JavaScript snippet to inject dynamic links into any DOM container.
11
- */
12
- static generateWidgetJs(apiBaseUrl: string): string {
13
- return `
14
- (function() {
15
- const container = document.getElementById('lynxseo-widget');
16
- if (!container) return;
17
-
18
- const domain = container.getAttribute('data-domain') || window.location.hostname;
19
- const currentPath = window.location.pathname;
20
-
21
- fetch('${apiBaseUrl}/api/widget/related?domain=' + encodeURIComponent(domain) + '&path=' + encodeURIComponent(currentPath))
22
- .then(res => res.json())
23
- .then(data => {
24
- if (!data || !data.links || data.links.length === 0) return;
25
-
26
- const nav = document.createElement('nav');
27
- nav.className = 'lynxseo-related-links';
28
- nav.style.cssText = 'margin: 2rem 0; padding: 1.5rem; background: #0f172a; border-radius: 0.75rem; border: 1px solid #1e293b; color: #f8fafc; font-family: sans-serif;';
29
-
30
- const title = document.createElement('h4');
31
- title.textContent = data.title || 'Solutions & Pages Recommandées';
32
- title.style.cssText = 'margin: 0 0 1rem; font-size: 1.1rem; color: #38bdf8; font-weight: 600;';
33
- nav.appendChild(title);
34
-
35
- const list = document.createElement('ul');
36
- list.style.cssText = 'list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.5rem;';
37
-
38
- data.links.forEach(link => {
39
- const li = document.createElement('li');
40
- const a = document.createElement('a');
41
- a.href = link.url;
42
- a.textContent = link.name;
43
- a.style.cssText = 'display: inline-block; padding: 0.4rem 0.8rem; background: #1e293b; color: #e2e8f0; text-decoration: none; border-radius: 0.375rem; font-size: 0.875rem; transition: background 0.2s;';
44
- a.onmouseover = () => { a.style.background = '#2563eb'; };
45
- a.onmouseout = () => { a.style.background = '#1e293b'; };
46
- li.appendChild(a);
47
- list.appendChild(li);
48
- });
49
-
50
- nav.appendChild(list);
51
- container.appendChild(nav);
52
- })
53
- .catch(err => console.error('LynxSEO Widget Error:', err));
54
- })();
55
- `.trim();
56
- }
57
- }
@@ -1,386 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { cleanSeoSlug, validateSeoSlug, MULTILINGUAL_STOP_WORDS } from "./slug-engine";
3
- import { LegalDisclaimerEngine, MULTILINGUAL_BANNED_DISPARAGING_WORDS } from "./legal-disclaimers";
4
- import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
5
- import { PseoMatrixEngine } from "./matrix-engine";
6
- import { getSeoAgentPrompt } from "./llm-prompt";
7
- import { UrlyticsEngine } from "./urlytics-engine";
8
- import { KeywordPermutatorEngine } from "./keyword-permutator";
9
- import { renderBrandIconSvg } from "./brand-icons";
10
- import { I18nDetector } from "./i18n-detector";
11
- import { renderUiIconSvg, resolveFeatureIcon } from "./ui-icons";
12
- import { OgImageGenerator } from "./og-image-generator";
13
- import { FreePublicToolsEngine } from "./free-public-tools";
14
-
15
- describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
16
- it("English: Strips stop words and years", () => {
17
- const slug = cleanSeoSlug("The best software for managing invoice payments in 2026", { language: "en" });
18
- expect(slug).toBe("best-software-managing-invoice-payments");
19
- });
20
-
21
- it("German: Strips German stop words (der, die, für, mit)", () => {
22
- const slug = cleanSeoSlug("Die beste Software für das Management mit Cloud", { language: "de" });
23
- expect(slug).toBe("beste-software-management-cloud");
24
- });
25
-
26
- it("Spanish: Strips Spanish stop words (el, la, para, con)", () => {
27
- const slug = cleanSeoSlug("El mejor software para la gestion con automatizacion", { language: "es" });
28
- expect(slug).toBe("mejor-software-gestion-automatizacion");
29
- });
30
-
31
- it("French: Strips French stop words (le, pour, avec)", () => {
32
- const slug = cleanSeoSlug("Le logiciel pour avocats avec automatisation", { language: "fr" });
33
- expect(slug).toBe("logiciel-avocats-automatisation");
34
- });
35
-
36
- it("Validates all 10 language dictionaries exist", () => {
37
- expect(Object.keys(MULTILINGUAL_STOP_WORDS).length).toBeGreaterThanOrEqual(10);
38
- expect(MULTILINGUAL_STOP_WORDS.en).toBeDefined();
39
- expect(MULTILINGUAL_STOP_WORDS.de).toBeDefined();
40
- expect(MULTILINGUAL_STOP_WORDS.es).toBeDefined();
41
- expect(MULTILINGUAL_STOP_WORDS.it).toBeDefined();
42
- expect(MULTILINGUAL_STOP_WORDS.pt).toBeDefined();
43
- });
44
- });
45
-
46
- describe("Multilingual Legal & Disclaimers Engine", () => {
47
- const legal = new LegalDisclaimerEngine({
48
- companyName: "Acme Cloud",
49
- correctionsEmail: "legal@acme.com",
50
- dataStalenessDays: 90,
51
- });
52
-
53
- it("Generates English disclaimer by default", () => {
54
- const notice = legal.getDisclaimer("pricing", "2026-08-01");
55
- expect(notice).toContain("Pricing disclaimer");
56
- expect(notice).toContain("2026-08-01");
57
- });
58
-
59
- it("Generates German disclaimer when requested", () => {
60
- const notice = legal.getDisclaimer("pricing", "2026-08-01", "de");
61
- expect(notice).toContain("Preishinweis");
62
- });
63
-
64
- it("Flags hazardous claims in English, German, French, and Spanish", () => {
65
- expect(legal.validateCompliance("This product is a scam", "Vendor").isCompliant).toBe(false);
66
- expect(legal.validateCompliance("Diese Software ist ein Betrug", "Vendor").isCompliant).toBe(false);
67
- expect(legal.validateCompliance("Ce logiciel est une arnaque", "Vendor").isCompliant).toBe(false);
68
- expect(legal.validateCompliance("Este producto es una estafa", "Vendor").isCompliant).toBe(false);
69
- });
70
-
71
- it("Verifies 10 languages supported in banned disparaging words dictionary", () => {
72
- expect(Object.keys(MULTILINGUAL_BANNED_DISPARAGING_WORDS).length).toBeGreaterThanOrEqual(10);
73
- });
74
- });
75
-
76
- describe("Extended Schema.org Builder", () => {
77
- it("Resolves 35+ specialized LocalBusiness types", () => {
78
- expect(ExtendedSchemaGraphBuilder.resolveBusinessType("plumber")).toBe("Plumber");
79
- expect(ExtendedSchemaGraphBuilder.resolveBusinessType("dentist")).toBe("Dentist");
80
- expect(ExtendedSchemaGraphBuilder.resolveBusinessType("lawyer")).toBe("LegalService");
81
- expect(ExtendedSchemaGraphBuilder.resolveBusinessType("hvac")).toBe("HVACBusiness");
82
- expect(ExtendedSchemaGraphBuilder.resolveBusinessType("accounting")).toBe("AccountingService");
83
- });
84
-
85
- it("Builds valid Organization with sameAs Entity SEO links", () => {
86
- const schema = ExtendedSchemaGraphBuilder.buildOrganization({
87
- name: "Acme",
88
- url: "https://acme.com",
89
- sameAs: ["https://wikidata.org/wiki/Q123"],
90
- });
91
- expect(schema["@type"]).toBe("Organization");
92
- expect(schema.sameAs).toContain("https://wikidata.org/wiki/Q123");
93
- });
94
-
95
- it("Builds complete Speakable schema for voice search & assistant", () => {
96
- const speakable = ExtendedSchemaGraphBuilder.buildSpeakable({
97
- cssSelectors: [".headline", ".direct-answer"],
98
- });
99
- expect(speakable["@type"]).toBe("SpeakableSpecification");
100
- expect(speakable.cssSelector).toEqual([".headline", ".direct-answer"]);
101
- });
102
-
103
- it("Builds complete PodcastEpisode and Restaurant schemas", () => {
104
- const podcast = ExtendedSchemaGraphBuilder.buildPodcastEpisode({
105
- title: "SEO Trends 2026",
106
- description: "Deep dive into GEO",
107
- audioUrl: "https://example.com/ep1.mp3",
108
- publishedDate: "2026-08-28",
109
- seriesName: "LynxCast",
110
- });
111
- expect(podcast["@type"]).toBe("PodcastEpisode");
112
- expect(podcast.name).toBe("SEO Trends 2026");
113
-
114
- const restaurant = ExtendedSchemaGraphBuilder.buildRestaurant({
115
- name: "Le Gourmet",
116
- url: "https://legourmet.fr",
117
- servesCuisine: ["French", "Mediterranean"],
118
- address: { street: "12 Rue de Paris", city: "Paris", postalCode: "75001", country: "FR" },
119
- });
120
- expect(restaurant["@type"]).toBe("Restaurant");
121
- expect(restaurant.servesCuisine).toContain("French");
122
- });
123
-
124
- it("Builds Book, Dataset, FactCheck, Carousel, and QAPage schemas", () => {
125
- const book = ExtendedSchemaGraphBuilder.buildBook({ name: "SEO in 2026", author: "LynxFlow" });
126
- expect(book["@type"]).toBe("Book");
127
-
128
- const dataset = ExtendedSchemaGraphBuilder.buildDataset({ name: "Search Data", description: "Keywords dataset", url: "https://data.com", keywords: ["seo", "ai"] });
129
- expect(dataset["@type"]).toBe("Dataset");
130
-
131
- const factCheck = ExtendedSchemaGraphBuilder.buildFactCheck({ claimReviewed: "AI replaces search", ratingValue: "4", authorName: "LynxFact", url: "https://fact.com" });
132
- expect(factCheck["@type"]).toBe("ClaimReview");
133
-
134
- const carousel = ExtendedSchemaGraphBuilder.buildCarousel([{ name: "Item 1", url: "https://item1.com", position: 1 }]);
135
- expect(carousel["@type"]).toBe("ItemList");
136
-
137
- const qa = ExtendedSchemaGraphBuilder.buildQAPage({ questionText: "How to rank #1?", authorName: "User1", answerText: "Use LynxSEO.", answerAuthor: "LynxBot" });
138
- expect(qa["@type"]).toBe("QAPage");
139
- });
140
- });
141
-
142
- describe("Master Programmatic Matrix Engine (English Default)", () => {
143
- const engine = new PseoMatrixEngine();
144
- const options = { brandName: "Acme", minPopulationToIndex: 15_000 };
145
-
146
- it("Generates English default URLs (/vs/, /alternatives/, /pricing/, /templates/, /glossary/)", () => {
147
- const pages = engine.generateAllMatrices(
148
- "https://acme.com",
149
- {
150
- competitors: [{ slug: "hubspot", name: "HubSpot", category: "CRM", drawbacks: [], advantagesOver: [] }],
151
- templates: [{ slug: "crm-pipeline-excel", title: "CRM Pipeline", topic: "Sales", format: "excel", benefits: ["Save time"] }],
152
- glossaryTerms: [{ slug: "mrr", term: "MRR", shortDefinition: "Monthly Recurring Revenue" }],
153
- },
154
- options,
155
- );
156
-
157
- const paths = pages.map((p) => p.urlPath);
158
- expect(paths).toContain("/vs/hubspot");
159
- expect(paths).toContain("/alternatives/hubspot");
160
- expect(paths).toContain("/pricing/hubspot");
161
- expect(paths).toContain("/templates/crm-pipeline-excel");
162
- expect(paths).toContain("/glossary/mrr");
163
- });
164
-
165
- it("Switches cleanly to French route prefixes when language is 'fr'", () => {
166
- const frOptions = { brandName: "Acme", language: "fr" };
167
- const pages = engine.generateAllMatrices(
168
- "https://acme.com",
169
- {
170
- competitors: [{ slug: "hubspot", name: "HubSpot", category: "CRM", drawbacks: [], advantagesOver: [] }],
171
- templates: [{ slug: "crm-pipeline-excel", title: "CRM Pipeline", topic: "Sales", format: "excel", benefits: ["Gain de temps"] }],
172
- },
173
- frOptions,
174
- );
175
-
176
- const paths = pages.map((p) => p.urlPath);
177
- expect(paths).toContain("/comparatif/hubspot");
178
- expect(paths).toContain("/tarifs/hubspot");
179
- expect(paths).toContain("/modeles/crm-pipeline-excel");
180
- });
181
-
182
- it("Generates comprehensive system prompt for LLMs and AI Agents", () => {
183
- const prompt = getSeoAgentPrompt({ brandName: "Acme", domain: "https://acme.com" });
184
- expect(prompt).toContain("Acme");
185
- expect(prompt).toContain("https://acme.com");
186
- expect(prompt).toContain("cleanSeoSlug");
187
- expect(prompt).toContain("Schema.org");
188
- });
189
- });
190
-
191
- describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
192
- it("UrlyticsEngine: Parses and decomposes URL hierarchy", () => {
193
- const parsed = UrlyticsEngine.parseUrl("https://acme.com/solutions/crm/fr/paris?src=seo&ref=top#pricing");
194
- expect(parsed.domain).toBe("acme.com");
195
- expect(parsed.depth).toBe(4);
196
- expect(parsed.dir1).toBe("solutions");
197
- expect(parsed.lastDir).toBe("paris");
198
- expect(parsed.queryParams.src).toBe("seo");
199
- expect(parsed.hashFragment).toBe("pricing");
200
- });
201
-
202
- it("KeywordPermutatorEngine: Generates combinatorial SEM matrix with intent", () => {
203
- const matrix = KeywordPermutatorEngine.generateKeywordMatrix({
204
- products: ["crm", "billing"],
205
- words: ["best", "pricing"],
206
- locations: ["paris"],
207
- });
208
- expect(matrix.length).toBeGreaterThan(5);
209
- const hasBestCrm = matrix.some((k) => k.keyword === "best crm" && k.intent === "commercial");
210
- expect(hasBestCrm).toBe(true);
211
- });
212
-
213
- it("BrandIcons: Resolves official SVG vector for Shopify, Facebook, Slack", () => {
214
- const shopifySvg = renderBrandIconSvg("shopify");
215
- expect(shopifySvg).toContain("<svg");
216
- expect(shopifySvg).toContain("viewBox");
217
-
218
- const fbSvg = renderBrandIconSvg("facebook");
219
- expect(fbSvg).toContain("<svg");
220
- });
221
-
222
- it("ExtendedSchemas: Builds AggregateRating and AggregateOffer (Santifer model)", () => {
223
- const rating = ExtendedSchemaGraphBuilder.buildAggregateRating({
224
- itemReviewedName: "Acme CRM",
225
- ratingValue: 4.9,
226
- reviewCount: 1280,
227
- });
228
- expect(rating["@type"]).toBe("AggregateRating");
229
- expect(rating.ratingValue).toBe(4.9);
230
-
231
- const offer = ExtendedSchemaGraphBuilder.buildAggregateOffer({
232
- lowPrice: 29,
233
- highPrice: 99,
234
- currency: "EUR",
235
- });
236
- expect(offer["@type"]).toBe("AggregateOffer");
237
- expect(offer.lowPrice).toBe("29");
238
- });
239
-
240
- it("PseoMatrixEngine: resolvePage returns complete page in < 0.05ms", () => {
241
- const engine = new PseoMatrixEngine();
242
- const page = engine.resolvePage(
243
- ["autopost-facebook", "lyon"],
244
- "https://acme.com",
245
- {
246
- services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
247
- locations: [{ slug: "lyon", name: "Lyon", country: "France", population: 500_000 }],
248
- },
249
- { brandName: "Acme", cleanDirectRoutes: true },
250
- );
251
- expect(page).toBeDefined();
252
- expect(page?.urlPath).toBe("/autopost-facebook/lyon");
253
- expect(page?.h1).toContain("Autopost Facebook");
254
- });
255
-
256
- it("PseoMatrixEngine: generateSitemapXml and generateLlmsTxt generate complete feeds", () => {
257
- const engine = new PseoMatrixEngine();
258
- const data = {
259
- services: [{ slug: "autopost", name: "Autopost", category: "Social" }],
260
- locations: [{ slug: "paris", name: "Paris", country: "France", population: 2_000_000 }],
261
- };
262
- const sitemap = engine.generateSitemapXml("https://acme.com", data, { brandName: "Acme" });
263
- expect(sitemap).toContain("<urlset");
264
- expect(sitemap).toContain("<loc>https://acme.com/autopost/paris</loc>");
265
-
266
- const llmsTxt = engine.generateLlmsTxt("https://acme.com", data, { brandName: "Acme" });
267
- expect(llmsTxt).toContain("# Acme — AI Knowledge Graph");
268
- expect(llmsTxt).toContain("https://acme.com/autopost/paris");
269
- });
270
-
271
- it("Built-in Locations: Auto-provisions cities without manual entry", () => {
272
- const engine = new PseoMatrixEngine();
273
- const pages = engine.generateAllMatrices(
274
- "https://acme.com",
275
- {
276
- services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
277
- },
278
- { brandName: "Acme", countries: ["france", "spain"] },
279
- );
280
-
281
- const paths = pages.map((p) => p.urlPath);
282
- expect(paths).toContain("/autopost-facebook/paris");
283
- expect(paths).toContain("/autopost-facebook/lyon");
284
- expect(paths).toContain("/autopost-facebook/madrid");
285
- expect(paths).toContain("/autopost-facebook/barcelona");
286
- });
287
-
288
- it("I18nDetector: Normalizes locales and extracts locale from pathname", () => {
289
- expect(I18nDetector.normalizeLocale("fr-FR")).toBe("fr");
290
- expect(I18nDetector.normalizeLocale("en_US")).toBe("en");
291
-
292
- const extraction = I18nDetector.extractLocaleFromPath("/fr/autopost-facebook/paris", ["fr", "en", "es"]);
293
- expect(extraction.locale).toBe("fr");
294
- expect(extraction.remainingPath).toBe("/autopost-facebook/paris");
295
- });
296
-
297
- it("PseoMatrixEngine: resolveServicePage, generateRobotsTxt, and getTelemetryPayload", () => {
298
- const engine = new PseoMatrixEngine();
299
- const data = {
300
- services: [{ slug: "crm", name: "CRM", category: "Sales" }],
301
- locations: [{ slug: "paris", name: "Paris", country: "France" }],
302
- };
303
-
304
- const servicePage = engine.resolveServicePage("crm", "paris", "https://acme.com", data, { brandName: "Acme" });
305
- expect(servicePage).toBeDefined();
306
- expect(servicePage?.h1).toContain("CRM");
307
-
308
- const robots = engine.generateRobotsTxt("https://acme.com", { allowAiBots: true });
309
- expect(robots).toContain("User-agent: GPTBot");
310
- expect(robots).toContain("Sitemap: https://acme.com/sitemap.xml");
311
-
312
- const telemetry = engine.getTelemetryPayload("https://acme.com", data, { brandName: "Acme" });
313
- expect(telemetry.totalPages).toBeGreaterThan(0);
314
- expect(telemetry.brandName).toBe("Acme");
315
- expect(telemetry.servicesCount).toBe(1);
316
- });
317
-
318
- it("UI Icons: Renders standard Lucide-style SVG and resolves semantic feature icons", () => {
319
- const shieldSvg = renderUiIconSvg("shield");
320
- expect(shieldSvg).toContain("<svg");
321
- expect(shieldSvg).toContain("viewBox");
322
-
323
- expect(resolveFeatureIcon("Bank-grade RGPD Security")).toBe("shield");
324
- expect(resolveFeatureIcon("Real-time automated sync")).toBe("zap");
325
- expect(resolveFeatureIcon("Analytics dashboard & KPI tracker")).toBe("chart");
326
- expect(resolveFeatureIcon("Smart AI content generator")).toBe("sparkles");
327
- });
328
-
329
- it("OgImageGenerator: Generates dynamic 1200x630 SVG and Edge URL query", () => {
330
- const ogSvg = OgImageGenerator.renderDynamicOgSvg({
331
- brandName: "Acme Cloud",
332
- title: "Autopost Facebook & Instagram in Paris",
333
- badgeText: "SOCIAL MEDIA",
334
- locationName: "Paris, France",
335
- ratingValue: 4.9,
336
- reviewCount: 1280,
337
- });
338
- expect(ogSvg).toContain("<svg width=\"1200\" height=\"630\"");
339
- expect(ogSvg).toContain("Autopost Facebook");
340
- expect(ogSvg).toContain("4.9 / 5");
341
- expect(ogSvg).toContain("Paris, France");
342
-
343
- const engine = new PseoMatrixEngine();
344
- const page = engine.resolvePage(
345
- ["autopost-facebook", "paris"],
346
- "https://acme.com",
347
- {
348
- services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
349
- },
350
- { brandName: "Acme", countries: ["france"] },
351
- );
352
- expect(page?.ogImageUrl).toBeDefined();
353
- expect(page?.ogImageUrl).toContain("/api/og?");
354
- expect(page?.ogImageUrl).toContain("brand=Acme");
355
- });
356
-
357
- it("FreePublicToolsEngine: Simulates SERP, calculates ROI, and analyzes density", () => {
358
- const serp = FreePublicToolsEngine.simulateSerpSnippet({
359
- title: "Autopost Facebook & Instagram Tool 2026",
360
- description: "Automate your social media scheduling across all major networks with AI copilot and analytics.",
361
- url: "https://acme.com/autopost-facebook",
362
- targetKeyword: "autopost",
363
- });
364
- expect(serp.estimatedCtrScore).toBeGreaterThan(50);
365
- expect(serp.titlePixelWidth).toBeGreaterThan(0);
366
-
367
- const roi = FreePublicToolsEngine.calculateRoi({
368
- hoursSpentPerWeek: 10,
369
- hourlyRateOrSalary: 50,
370
- teamMembersCount: 2,
371
- softwareMonthlyPrice: 79,
372
- });
373
- expect(roi.monthlyCostSavings).toBeGreaterThan(1000);
374
- expect(roi.roiPercentage).toBeGreaterThan(100);
375
-
376
- const density = FreePublicToolsEngine.analyzeTextDensity(
377
- "Autopost Facebook automatise vos réseaux sociaux. Notre outil autopost vous fait gagner du temps avec autopost.",
378
- "fr",
379
- );
380
- expect(density.totalWords).toBeGreaterThan(5);
381
- expect(density.topKeywords[0].word).toBe("autopost");
382
-
383
- const schema = FreePublicToolsEngine.generateSampleSchema("softwareApp", "Acme", "https://acme.com");
384
- expect(schema["@type"]).toBe("SoftwareApplication");
385
- });
386
- });