@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.
- 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 -2618
- 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/crosslink-scorer.ts
DELETED
|
@@ -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
|
-
}
|
package/src/engine.test.ts
DELETED
|
@@ -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
|
-
});
|