@lynxflow/seo-engine 1.4.1 → 1.5.1
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 +84 -339
- package/TUTORIEL_INTEGRATION_SITE.md +283 -0
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/dist/ad-intelligence-cro.d.ts +58 -0
- package/dist/ai-bots-log-analyzer.d.ts +36 -0
- package/dist/analytics-client.d.ts +48 -11
- package/dist/auth-key.d.ts +9 -0
- package/dist/brand-dna-calendar.d.ts +35 -0
- package/dist/copy-frameworks-master.d.ts +56 -0
- package/dist/cro-copywriting-engine.d.ts +46 -0
- package/dist/crosslink-scorer.d.ts +33 -0
- package/dist/embeddable-seo-widget.d.ts +12 -0
- package/dist/engine.d.ts +29 -1
- package/dist/engine.test.d.ts +1 -0
- package/dist/extended-schemas.d.ts +106 -0
- package/dist/geo-mesh-linking.d.ts +33 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +4173 -128
- package/dist/index.mjs +4101 -98
- package/dist/instant-matrix-search.d.ts +19 -0
- package/dist/isr-cache-manager.d.ts +35 -0
- package/dist/knowledge-graph-linker.d.ts +29 -0
- package/dist/legal-disclaimers.d.ts +46 -0
- package/dist/llm-content-cleaner.d.ts +16 -0
- package/dist/llm-prompt.d.ts +13 -0
- package/dist/master-marketing-engine.d.ts +114 -0
- package/dist/matrix-engine.d.ts +201 -0
- package/dist/mcp-seo-server.d.ts +21 -0
- package/dist/ngram-density-analyzer.d.ts +35 -0
- package/dist/rank-math-parity.d.ts +58 -0
- package/dist/real-reviews-sync.d.ts +73 -0
- package/dist/rss-syndication-feed.d.ts +25 -0
- package/dist/schema-builder.d.ts +6 -0
- package/dist/seo-opportunities-decay.d.ts +54 -0
- package/dist/serp-history-alerts.d.ts +27 -0
- package/dist/slug-engine.d.ts +32 -0
- package/dist/social-ads-seo.d.ts +77 -0
- package/dist/social-growth-suite.d.ts +125 -0
- package/dist/social-trend-seo.d.ts +38 -0
- package/dist/social-video-seo.d.ts +62 -0
- package/dist/team-rbac.d.ts +17 -0
- package/dist/technical-rules-auditor.d.ts +50 -0
- package/dist/types.d.ts +4 -44
- package/dist/yoast-parity.d.ts +45 -0
- package/package.json +8 -5
- package/src/ad-intelligence-cro.ts +140 -0
- package/src/ai-bots-log-analyzer.ts +127 -0
- package/src/analytics-client.ts +222 -48
- package/src/auth-key.ts +59 -0
- package/src/brand-dna-calendar.ts +120 -0
- package/src/copy-frameworks-master.ts +89 -0
- package/src/cro-copywriting-engine.ts +105 -0
- package/src/crosslink-scorer.ts +101 -0
- package/src/embeddable-seo-widget.ts +57 -0
- package/src/engine.test.ts +136 -0
- package/src/engine.ts +218 -27
- package/src/extended-schemas.ts +279 -0
- package/src/geo-mesh-linking.ts +93 -0
- package/src/index.ts +89 -0
- package/src/instant-matrix-search.ts +48 -0
- package/src/isr-cache-manager.ts +78 -0
- package/src/knowledge-graph-linker.ts +81 -0
- package/src/legal-disclaimers.ts +407 -0
- package/src/llm-content-cleaner.ts +64 -0
- package/src/llm-prompt.ts +81 -0
- package/src/master-marketing-engine.ts +314 -0
- package/src/matrix-engine.ts +818 -0
- package/src/mcp-seo-server.ts +108 -0
- package/src/ngram-density-analyzer.ts +108 -0
- package/src/rank-math-parity.ts +235 -0
- package/src/real-reviews-sync.ts +207 -0
- package/src/rss-syndication-feed.ts +56 -0
- package/src/schema-builder.ts +71 -24
- package/src/seo-opportunities-decay.ts +154 -0
- package/src/serp-history-alerts.ts +75 -0
- package/src/slug-engine.ts +183 -0
- package/src/social-ads-seo.ts +211 -0
- package/src/social-growth-suite.ts +310 -0
- package/src/social-trend-seo.ts +103 -0
- package/src/social-video-seo.ts +100 -0
- package/src/team-rbac.ts +61 -0
- package/src/technical-rules-auditor.ts +171 -0
- package/src/types.ts +22 -47
- package/src/yoast-parity.ts +161 -0
- package/tsconfig.json +2 -1
package/src/auth-key.ts
CHANGED
|
@@ -45,6 +45,21 @@ export class ApiKeyGuardian {
|
|
|
45
45
|
return `ba_key_${tier}_${tenantId}_${checksum}`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 🛡️ Generates an Admin Bypass / Free Trial API key with a hard expiration date (YYYYMMDD).
|
|
50
|
+
* Bypasses Lago/Hyperswitch payment verification until the expiration date.
|
|
51
|
+
*/
|
|
52
|
+
static generateAdminTrialBypassKey(opts: {
|
|
53
|
+
tenantId: string;
|
|
54
|
+
expiresAtIso: string; // e.g. "2026-12-31"
|
|
55
|
+
tier?: "starter" | "growth" | "enterprise";
|
|
56
|
+
}): string {
|
|
57
|
+
const tier = opts.tier || "growth";
|
|
58
|
+
const dateStr = opts.expiresAtIso.replace(/-/g, "").substring(0, 8); // "20261231"
|
|
59
|
+
const checksum = this.computeChecksum(`${opts.tenantId}:${tier}:${dateStr}`);
|
|
60
|
+
return `ba_trial_${tier}_${opts.tenantId}_${dateStr}_${checksum}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
/**
|
|
49
64
|
* Fast cryptographic validation of Better Auth API keys.
|
|
50
65
|
*/
|
|
@@ -63,6 +78,50 @@ export class ApiKeyGuardian {
|
|
|
63
78
|
|
|
64
79
|
const cleanKey = apiKey.trim();
|
|
65
80
|
|
|
81
|
+
// 0. Admin Trial / Payment Bypass Keys with Expiry (ba_trial_...)
|
|
82
|
+
if (cleanKey.startsWith("ba_trial_")) {
|
|
83
|
+
const parts = cleanKey.split("_");
|
|
84
|
+
// Format: ba_trial_{tier}_{tenantId}_{YYYYMMDD}_{checksum}
|
|
85
|
+
const tier = (parts[2] as "starter" | "growth" | "enterprise") || "growth";
|
|
86
|
+
const tenantId = parts[3] || "trial_tenant";
|
|
87
|
+
const dateStr = parts[4]; // "20261231"
|
|
88
|
+
|
|
89
|
+
if (dateStr && dateStr.length === 8) {
|
|
90
|
+
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
91
|
+
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
|
|
92
|
+
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
93
|
+
const expiryDate = new Date(Date.UTC(year, month, day, 23, 59, 59));
|
|
94
|
+
const now = new Date();
|
|
95
|
+
|
|
96
|
+
if (now > expiryDate) {
|
|
97
|
+
return {
|
|
98
|
+
isValid: false,
|
|
99
|
+
tier,
|
|
100
|
+
tenantId,
|
|
101
|
+
maxPages: 0,
|
|
102
|
+
maxDomains: 0,
|
|
103
|
+
monthlyCreditBudget: 0,
|
|
104
|
+
tokenManager: new TokenQuotaManager("starter"),
|
|
105
|
+
errorMessage: `Admin Trial Expired: This license expired on ${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}. Please renew or activate a paid subscription.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const maxPages = tier === "enterprise" ? 5_000_000 : tier === "starter" ? 50_000 : 500_000;
|
|
111
|
+
const maxDomains = tier === "enterprise" ? 9999 : tier === "starter" ? 1 : 3;
|
|
112
|
+
const budget = tier === "enterprise" ? 50_000 : tier === "starter" ? 1_000 : 5_000;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
isValid: true,
|
|
116
|
+
tier,
|
|
117
|
+
tenantId,
|
|
118
|
+
maxPages,
|
|
119
|
+
maxDomains,
|
|
120
|
+
monthlyCreditBudget: budget,
|
|
121
|
+
tokenManager: new TokenQuotaManager(tier),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
66
125
|
// 1. Enterprise / Master Admin Keys
|
|
67
126
|
if (cleanKey.startsWith("ba_admin_") || cleanKey.startsWith("lynx_enterprise_") || cleanKey.startsWith("lynx_live_") || cleanKey.includes("_enterprise_")) {
|
|
68
127
|
return {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🧬 Brand DNA Extractor & Content Calendar Engine (Pole 4)
|
|
3
|
+
*
|
|
4
|
+
* Scrapes and analyzes client domain to extract Unique Value Proposition (UVP),
|
|
5
|
+
* target personas, editorial tone, and generates an automated 4-week content calendar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface BrandDnaProfile {
|
|
9
|
+
domain: string;
|
|
10
|
+
brandName: string;
|
|
11
|
+
tagline: string;
|
|
12
|
+
uniqueValueProposition: string;
|
|
13
|
+
targetPersonas: string[];
|
|
14
|
+
editorialTone: "authoritative" | "direct" | "technical" | "pedagogical" | "empathetic";
|
|
15
|
+
keyDifferentiators: string[];
|
|
16
|
+
primaryIndustry: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ContentCalendarItem {
|
|
20
|
+
id: string;
|
|
21
|
+
weekNumber: number;
|
|
22
|
+
scheduledDay: string;
|
|
23
|
+
matrixFamily: string;
|
|
24
|
+
title: string;
|
|
25
|
+
targetUrl: string;
|
|
26
|
+
primaryKeyword: string;
|
|
27
|
+
status: "draft" | "ready" | "scheduled" | "published";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class BrandDnaCalendarEngine {
|
|
31
|
+
/**
|
|
32
|
+
* Generates a complete 4-week editorial content calendar based on the Brand DNA and 8 matrices.
|
|
33
|
+
*/
|
|
34
|
+
static generate4WeekCalendar(
|
|
35
|
+
dna: BrandDnaProfile,
|
|
36
|
+
services: { slug: string; name: string }[],
|
|
37
|
+
topCities: string[] = ["Paris", "Lyon", "Marseille", "Bordeaux"],
|
|
38
|
+
): ContentCalendarItem[] {
|
|
39
|
+
const calendar: ContentCalendarItem[] = [];
|
|
40
|
+
const days = ["Lundi", "Mercredi", "Vendredi"];
|
|
41
|
+
let itemId = 1;
|
|
42
|
+
|
|
43
|
+
for (let week = 1; week <= 4; week++) {
|
|
44
|
+
for (const day of days) {
|
|
45
|
+
let matrixFamily = "local-geo";
|
|
46
|
+
let title = "";
|
|
47
|
+
let targetUrl = "";
|
|
48
|
+
let primaryKeyword = "";
|
|
49
|
+
const svc = services[(itemId - 1) % services.length];
|
|
50
|
+
|
|
51
|
+
switch (itemId % 8) {
|
|
52
|
+
case 1:
|
|
53
|
+
matrixFamily = "local-geo";
|
|
54
|
+
const city = topCities[(week - 1) % topCities.length];
|
|
55
|
+
title = `${svc.name} à ${city} — Solution Pro`;
|
|
56
|
+
targetUrl = `/solutions/${svc.slug}/fr/${city.toLowerCase()}`;
|
|
57
|
+
primaryKeyword = `${svc.name} ${city}`;
|
|
58
|
+
break;
|
|
59
|
+
case 2:
|
|
60
|
+
matrixFamily = "vs-comparison";
|
|
61
|
+
title = `${dna.brandName} vs HubSpot : Comparatif Détaillé`;
|
|
62
|
+
targetUrl = `/comparatif/${svc.slug}-vs-hubspot`;
|
|
63
|
+
primaryKeyword = `${svc.name} vs hubspot`;
|
|
64
|
+
break;
|
|
65
|
+
case 3:
|
|
66
|
+
matrixFamily = "alternative";
|
|
67
|
+
title = `Meilleure Alternative à Aircall en 2026`;
|
|
68
|
+
targetUrl = `/alternatives/alternative-a-aircall`;
|
|
69
|
+
primaryKeyword = `alternative aircall`;
|
|
70
|
+
break;
|
|
71
|
+
case 4:
|
|
72
|
+
matrixFamily = "industry";
|
|
73
|
+
title = `Solution ${svc.name} pour Cabinets d'Avocats`;
|
|
74
|
+
targetUrl = `/secteurs/${svc.slug}-pour-avocats`;
|
|
75
|
+
primaryKeyword = `${svc.name} avocats`;
|
|
76
|
+
break;
|
|
77
|
+
case 5:
|
|
78
|
+
matrixFamily = "integration";
|
|
79
|
+
title = `Intégration ${svc.name} & Shopify`;
|
|
80
|
+
targetUrl = `/integrations/${svc.slug}-avec-shopify`;
|
|
81
|
+
primaryKeyword = `${svc.name} shopify`;
|
|
82
|
+
break;
|
|
83
|
+
case 6:
|
|
84
|
+
matrixFamily = "persona";
|
|
85
|
+
title = `Espace de travail ${svc.name} pour Directeur Commercial`;
|
|
86
|
+
targetUrl = `/metiers/${svc.slug}-pour-directeur-commercial`;
|
|
87
|
+
primaryKeyword = `${svc.name} directeur commercial`;
|
|
88
|
+
break;
|
|
89
|
+
case 7:
|
|
90
|
+
matrixFamily = "use-case";
|
|
91
|
+
title = `Comment Automatiser la Relance de Devis avec ${dna.brandName}`;
|
|
92
|
+
targetUrl = `/cas-usage/${svc.slug}-relance-devis`;
|
|
93
|
+
primaryKeyword = `automatiser relance devis`;
|
|
94
|
+
break;
|
|
95
|
+
case 0:
|
|
96
|
+
matrixFamily = "calculator";
|
|
97
|
+
title = `Simulateur Gratuit de ROI & Gain de Temps`;
|
|
98
|
+
targetUrl = `/outils/simulateur-roi-${svc.slug}`;
|
|
99
|
+
primaryKeyword = `calculateur roi ${svc.name}`;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
calendar.push({
|
|
104
|
+
id: `cal-${itemId}`,
|
|
105
|
+
weekNumber: week,
|
|
106
|
+
scheduledDay: day,
|
|
107
|
+
matrixFamily,
|
|
108
|
+
title,
|
|
109
|
+
targetUrl,
|
|
110
|
+
primaryKeyword,
|
|
111
|
+
status: week === 1 ? "ready" : "scheduled",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
itemId++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return calendar;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✍️ Master Copywriting & AI-SEO Prompt Frameworks Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements the complete set of formulas from marketingskills:
|
|
5
|
+
* 1. 5 Battle-Tested Headline Formulas (Outcome, Problem, Audience, Differentiation, Proof)
|
|
6
|
+
* 2. The Human Action Model (Discomfort ➔ Vision ➔ Path)
|
|
7
|
+
* 3. 40-60 Word Concise AEO / GEO Definition Answer Block for LLM Citation
|
|
8
|
+
* 4. Google AI Query Fan-Out Generator (5-10 concurrent semantic sub-queries)
|
|
9
|
+
* 5. Value Equation & Grand Slam Offer Stack (Alex Hormozi)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface HeadlineVariants {
|
|
13
|
+
outcomeFocused: string; // "{Achieve outcome} without {pain point}"
|
|
14
|
+
problemFocused: string; // "Stop [pain]. Start [pleasure]."
|
|
15
|
+
audienceFocused: string; // "{Service} pensé pour {target audience}"
|
|
16
|
+
differentiationFocused: string; // "La méthode zéro-base de données pour {outcome}"
|
|
17
|
+
proofFocused: string; // "Rejoignez 1 200+ entreprises qui ont {outcome}"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HumanActionModelCopy {
|
|
21
|
+
discomfortState: string; // Current painful friction
|
|
22
|
+
visionState: string; // Desirable effortless future
|
|
23
|
+
actionPath: string; // Immediate clear step
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CopywritingFrameworksMaster {
|
|
27
|
+
/**
|
|
28
|
+
* 1. Generates 5 high-converting headline variants based on marketingskills formulas.
|
|
29
|
+
*/
|
|
30
|
+
static generateHeadlines(opts: {
|
|
31
|
+
serviceName: string;
|
|
32
|
+
desiredOutcome: string;
|
|
33
|
+
painPoint: string;
|
|
34
|
+
targetAudience: string;
|
|
35
|
+
metricProof: string;
|
|
36
|
+
}): HeadlineVariants {
|
|
37
|
+
return {
|
|
38
|
+
outcomeFocused: `${opts.desiredOutcome} sans ${opts.painPoint}`,
|
|
39
|
+
problemFocused: `Arrêtez ${opts.painPoint}. Commencez à ${opts.desiredOutcome}.`,
|
|
40
|
+
audienceFocused: `${opts.serviceName} pour ${opts.targetAudience} : ${opts.desiredOutcome}`,
|
|
41
|
+
differentiationFocused: `Le moyen le plus rapide de ${opts.desiredOutcome} sans configuration complexe`,
|
|
42
|
+
proofFocused: `${opts.metricProof} font confiance à ${opts.serviceName} pour ${opts.desiredOutcome}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 2. Human Action Model (Discomfort ➔ Vision ➔ Path).
|
|
48
|
+
*/
|
|
49
|
+
static generateHumanActionModel(opts: {
|
|
50
|
+
painPoint: string;
|
|
51
|
+
desiredOutcome: string;
|
|
52
|
+
solutionName: string;
|
|
53
|
+
timeframe: string;
|
|
54
|
+
}): HumanActionModelCopy {
|
|
55
|
+
return {
|
|
56
|
+
discomfortState: `Aujourd'hui, vous perdez un temps précieux avec ${opts.painPoint}, ce qui freine directement votre croissance.`,
|
|
57
|
+
visionState: `Imaginez pouvoir ${opts.desiredOutcome} automatiquement et recevoir des prospects qualifiés 24/7.`,
|
|
58
|
+
actionPath: `Activez ${opts.solutionName} en moins de ${opts.timeframe} et débloquez votre plein potentiel d'acquisition.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 3. 40-60 Word Direct-Answer GEO Block (Gold standard for ChatGPT, Perplexity & Google AI Overviews).
|
|
64
|
+
*/
|
|
65
|
+
static generateAeoAnswerBlock(opts: {
|
|
66
|
+
term: string;
|
|
67
|
+
definition: string;
|
|
68
|
+
keyBenefit: string;
|
|
69
|
+
proofStat: string;
|
|
70
|
+
}): string {
|
|
71
|
+
return `${opts.term} est ${opts.definition}. Cette solution permet de ${opts.keyBenefit}, enregistrant ${opts.proofStat}. Elle est conçue pour optimiser l'indexation sur les moteurs traditionnels et les réponses de recherche IA (ChatGPT Search, Perplexity).`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 4. Query Fan-Out Generator: 8 related queries that Google AI Overviews fan out to.
|
|
76
|
+
*/
|
|
77
|
+
static generateQueryFanOut(topic: string, serviceSlug: string): string[] {
|
|
78
|
+
return [
|
|
79
|
+
`Comment fonctionne ${topic} ?`,
|
|
80
|
+
`Meilleur outil pour ${topic} en 2026`,
|
|
81
|
+
`Prix et tarif ${serviceSlug}`,
|
|
82
|
+
`Avis clients et comparatif ${serviceSlug}`,
|
|
83
|
+
`Alternatives à ${serviceSlug}`,
|
|
84
|
+
`Intégration ${topic} avec CRM et site web`,
|
|
85
|
+
`Comment automatiser ${topic} sans code ?`,
|
|
86
|
+
`Calculateur de ROI pour ${topic}`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✍️ Automated Copywriting & CRO Injection Engine (Inspiration: marketingskills)
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates high-converting sales copy blocks using proven frameworks:
|
|
5
|
+
* 1. PAS (Problem - Agitation - Solution)
|
|
6
|
+
* 2. AIDA (Attention - Interest - Desire - Action)
|
|
7
|
+
* 3. Trust & Social Proof Elements (Risk Reducers, Guarantees, Badges)
|
|
8
|
+
* 4. High-Conversion Sticky CTAs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface CopywritingInput {
|
|
12
|
+
brandName: string;
|
|
13
|
+
solutionName: string;
|
|
14
|
+
targetNicheOrCity: string;
|
|
15
|
+
painPoint: string;
|
|
16
|
+
agitation: string;
|
|
17
|
+
outcome: string;
|
|
18
|
+
priceMonthly?: number;
|
|
19
|
+
currencySymbol?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CroCopywritingEngine {
|
|
23
|
+
/**
|
|
24
|
+
* Returns structured PAS (Problem - Agitation - Solution) text data.
|
|
25
|
+
*/
|
|
26
|
+
static generatePasCopy(solutionName: string, targetNiche: string): { problem: string; agitation: string; solution: string } {
|
|
27
|
+
return {
|
|
28
|
+
problem: `Les professionnels de ${targetNiche} perdent jusqu'à 15 heures par semaine sur des tâches manuelles répétitives.`,
|
|
29
|
+
agitation: `Sans automatisation, les relances sont oubliées, les opportunités clients s'évaporent et la rentabilité stagne.`,
|
|
30
|
+
solution: `${solutionName} automatise 100% de vos processus pour vous permettre de vous concentrer sur vos clients et votre croissance.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a PAS (Problem - Agitation - Solution) sales block in HTML.
|
|
36
|
+
*/
|
|
37
|
+
static generatePasBlock(input: CopywritingInput): string {
|
|
38
|
+
return `
|
|
39
|
+
<section class="cro-pas-framework my-8 p-6 bg-slate-900 border border-slate-800 rounded-xl">
|
|
40
|
+
<div class="pas-problem mb-4">
|
|
41
|
+
<span class="inline-block text-xs font-semibold px-2 py-1 bg-red-950 text-red-400 rounded">Le Problème</span>
|
|
42
|
+
<h3 class="text-xl font-bold text-white mt-2">${input.painPoint}</h3>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="pas-agitation mb-4 text-slate-300">
|
|
45
|
+
<p>${input.agitation}</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="pas-solution mt-4 pt-4 border-t border-slate-800">
|
|
48
|
+
<span class="inline-block text-xs font-semibold px-2 py-1 bg-emerald-950 text-emerald-400 rounded">La Solution ${input.brandName}</span>
|
|
49
|
+
<h4 class="text-lg font-bold text-white mt-1">${input.solutionName} pour ${input.targetNicheOrCity}</h4>
|
|
50
|
+
<p class="text-emerald-300 font-medium mt-1">➔ ${input.outcome}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
`.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates a High-Converting Social Proof & Trust Badges block.
|
|
58
|
+
*/
|
|
59
|
+
static generateTrustBadgesBlock(input: {
|
|
60
|
+
guaranteeDays?: number;
|
|
61
|
+
rating?: string;
|
|
62
|
+
reviewCount?: number;
|
|
63
|
+
freeTrialDays?: number;
|
|
64
|
+
}): string {
|
|
65
|
+
const guarantee = input.guaranteeDays ? `Garantie Satisfait ou Remboursé ${input.guaranteeDays} Jours` : "Sans engagement";
|
|
66
|
+
const trial = input.freeTrialDays ? `Essai gratuit ${input.freeTrialDays} jours sans carte bancaire` : "Déploiement immédiat";
|
|
67
|
+
|
|
68
|
+
return `
|
|
69
|
+
<div class="cro-trust-badges flex flex-wrap gap-4 items-center justify-center my-6 text-sm text-slate-300">
|
|
70
|
+
<div class="flex items-center gap-2 bg-slate-800/80 px-3 py-1.5 rounded-lg border border-slate-700">
|
|
71
|
+
<span>🔒</span>
|
|
72
|
+
<span>Paiement 100% Sécurisé & RGPD</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="flex items-center gap-2 bg-slate-800/80 px-3 py-1.5 rounded-lg border border-slate-700">
|
|
75
|
+
<span>⚡</span>
|
|
76
|
+
<span>${trial}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex items-center gap-2 bg-slate-800/80 px-3 py-1.5 rounded-lg border border-slate-700">
|
|
79
|
+
<span>🛡️</span>
|
|
80
|
+
<span>${guarantee}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
`.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generates an Instant WhatsApp / Direct Call conversion trigger button (inspired by Jacobo).
|
|
88
|
+
*/
|
|
89
|
+
static generateInstantConverterWidget(phoneNumber: string, brandOrMessage: string, serviceName?: string): string {
|
|
90
|
+
const cleanPhone = phoneNumber.replace(/[^0-9+]/g, "");
|
|
91
|
+
const msg = serviceName
|
|
92
|
+
? `Bonjour ${brandOrMessage}, je souhaite en savoir plus sur votre solution ${serviceName}.`
|
|
93
|
+
: brandOrMessage;
|
|
94
|
+
const encodedMsg = encodeURIComponent(msg);
|
|
95
|
+
|
|
96
|
+
return `
|
|
97
|
+
<div class="instant-converter-widget fixed bottom-6 right-6 z-50 flex flex-col gap-2">
|
|
98
|
+
<a href="https://wa.me/${cleanPhone}?text=${encodedMsg}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 px-4 py-3 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-full shadow-2xl transition duration-200">
|
|
99
|
+
<svg class="w-6 h-6 fill-current" viewBox="0 0 24 24"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981z"/></svg>
|
|
100
|
+
<span>Discuter en direct</span>
|
|
101
|
+
</a>
|
|
102
|
+
</div>
|
|
103
|
+
`.trim();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
|
|
8
|
+
describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
|
|
9
|
+
it("English: Strips stop words and years", () => {
|
|
10
|
+
const slug = cleanSeoSlug("The best software for managing invoice payments in 2026", { language: "en" });
|
|
11
|
+
expect(slug).toBe("best-software-managing-invoice-payments");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("German: Strips German stop words (der, die, für, mit)", () => {
|
|
15
|
+
const slug = cleanSeoSlug("Die beste Software für das Management mit Cloud", { language: "de" });
|
|
16
|
+
expect(slug).toBe("beste-software-management-cloud");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("Spanish: Strips Spanish stop words (el, la, para, con)", () => {
|
|
20
|
+
const slug = cleanSeoSlug("El mejor software para la gestion con automatizacion", { language: "es" });
|
|
21
|
+
expect(slug).toBe("mejor-software-gestion-automatizacion");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("French: Strips French stop words (le, pour, avec)", () => {
|
|
25
|
+
const slug = cleanSeoSlug("Le logiciel pour avocats avec automatisation", { language: "fr" });
|
|
26
|
+
expect(slug).toBe("logiciel-avocats-automatisation");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("Validates all 10 language dictionaries exist", () => {
|
|
30
|
+
expect(Object.keys(MULTILINGUAL_STOP_WORDS).length).toBeGreaterThanOrEqual(10);
|
|
31
|
+
expect(MULTILINGUAL_STOP_WORDS.en).toBeDefined();
|
|
32
|
+
expect(MULTILINGUAL_STOP_WORDS.de).toBeDefined();
|
|
33
|
+
expect(MULTILINGUAL_STOP_WORDS.es).toBeDefined();
|
|
34
|
+
expect(MULTILINGUAL_STOP_WORDS.it).toBeDefined();
|
|
35
|
+
expect(MULTILINGUAL_STOP_WORDS.pt).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("Multilingual Legal & Disclaimers Engine", () => {
|
|
40
|
+
const legal = new LegalDisclaimerEngine({
|
|
41
|
+
companyName: "Acme Cloud",
|
|
42
|
+
correctionsEmail: "legal@acme.com",
|
|
43
|
+
dataStalenessDays: 90,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("Generates English disclaimer by default", () => {
|
|
47
|
+
const notice = legal.getDisclaimer("pricing", "2026-08-01");
|
|
48
|
+
expect(notice).toContain("Pricing disclaimer");
|
|
49
|
+
expect(notice).toContain("2026-08-01");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("Generates German disclaimer when requested", () => {
|
|
53
|
+
const notice = legal.getDisclaimer("pricing", "2026-08-01", "de");
|
|
54
|
+
expect(notice).toContain("Preishinweis");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("Flags hazardous claims in English, German, French, and Spanish", () => {
|
|
58
|
+
expect(legal.validateCompliance("This product is a scam", "Vendor").isCompliant).toBe(false);
|
|
59
|
+
expect(legal.validateCompliance("Diese Software ist ein Betrug", "Vendor").isCompliant).toBe(false);
|
|
60
|
+
expect(legal.validateCompliance("Ce logiciel est une arnaque", "Vendor").isCompliant).toBe(false);
|
|
61
|
+
expect(legal.validateCompliance("Este producto es una estafa", "Vendor").isCompliant).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("Verifies 10 languages supported in banned disparaging words dictionary", () => {
|
|
65
|
+
expect(Object.keys(MULTILINGUAL_BANNED_DISPARAGING_WORDS).length).toBeGreaterThanOrEqual(10);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Extended Schema.org Builder", () => {
|
|
70
|
+
it("Resolves 35+ specialized LocalBusiness types", () => {
|
|
71
|
+
expect(ExtendedSchemaGraphBuilder.resolveBusinessType("plumber")).toBe("Plumber");
|
|
72
|
+
expect(ExtendedSchemaGraphBuilder.resolveBusinessType("dentist")).toBe("Dentist");
|
|
73
|
+
expect(ExtendedSchemaGraphBuilder.resolveBusinessType("lawyer")).toBe("LegalService");
|
|
74
|
+
expect(ExtendedSchemaGraphBuilder.resolveBusinessType("hvac")).toBe("HVACBusiness");
|
|
75
|
+
expect(ExtendedSchemaGraphBuilder.resolveBusinessType("accounting")).toBe("AccountingService");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("Builds valid Organization with sameAs Entity SEO links", () => {
|
|
79
|
+
const schema = ExtendedSchemaGraphBuilder.buildOrganization({
|
|
80
|
+
name: "Acme",
|
|
81
|
+
url: "https://acme.com",
|
|
82
|
+
sameAs: ["https://wikidata.org/wiki/Q123"],
|
|
83
|
+
});
|
|
84
|
+
expect(schema["@type"]).toBe("Organization");
|
|
85
|
+
expect(schema.sameAs).toContain("https://wikidata.org/wiki/Q123");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Master Programmatic Matrix Engine (English Default)", () => {
|
|
90
|
+
const engine = new PseoMatrixEngine();
|
|
91
|
+
const options = { brandName: "Acme", minPopulationToIndex: 15_000 };
|
|
92
|
+
|
|
93
|
+
it("Generates English default URLs (/vs/, /alternatives/, /pricing/, /templates/, /glossary/)", () => {
|
|
94
|
+
const pages = engine.generateAllMatrices(
|
|
95
|
+
"https://acme.com",
|
|
96
|
+
{
|
|
97
|
+
competitors: [{ slug: "hubspot", name: "HubSpot", category: "CRM", drawbacks: [], advantagesOver: [] }],
|
|
98
|
+
templates: [{ slug: "crm-pipeline-excel", title: "CRM Pipeline", topic: "Sales", format: "excel", benefits: ["Save time"] }],
|
|
99
|
+
glossaryTerms: [{ slug: "mrr", term: "MRR", shortDefinition: "Monthly Recurring Revenue" }],
|
|
100
|
+
},
|
|
101
|
+
options,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const paths = pages.map((p) => p.urlPath);
|
|
105
|
+
expect(paths).toContain("/vs/hubspot");
|
|
106
|
+
expect(paths).toContain("/alternatives/hubspot");
|
|
107
|
+
expect(paths).toContain("/pricing/hubspot");
|
|
108
|
+
expect(paths).toContain("/templates/crm-pipeline-excel");
|
|
109
|
+
expect(paths).toContain("/glossary/mrr");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("Switches cleanly to French route prefixes when language is 'fr'", () => {
|
|
113
|
+
const frOptions = { brandName: "Acme", language: "fr" };
|
|
114
|
+
const pages = engine.generateAllMatrices(
|
|
115
|
+
"https://acme.com",
|
|
116
|
+
{
|
|
117
|
+
competitors: [{ slug: "hubspot", name: "HubSpot", category: "CRM", drawbacks: [], advantagesOver: [] }],
|
|
118
|
+
templates: [{ slug: "crm-pipeline-excel", title: "CRM Pipeline", topic: "Sales", format: "excel", benefits: ["Gain de temps"] }],
|
|
119
|
+
},
|
|
120
|
+
frOptions,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const paths = pages.map((p) => p.urlPath);
|
|
124
|
+
expect(paths).toContain("/comparatif/hubspot");
|
|
125
|
+
expect(paths).toContain("/tarifs/hubspot");
|
|
126
|
+
expect(paths).toContain("/modeles/crm-pipeline-excel");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("Generates comprehensive system prompt for LLMs and AI Agents", () => {
|
|
130
|
+
const prompt = getSeoAgentPrompt({ brandName: "Acme", domain: "https://acme.com" });
|
|
131
|
+
expect(prompt).toContain("Acme");
|
|
132
|
+
expect(prompt).toContain("https://acme.com");
|
|
133
|
+
expect(prompt).toContain("cleanSeoSlug");
|
|
134
|
+
expect(prompt).toContain("Schema.org");
|
|
135
|
+
});
|
|
136
|
+
});
|