@lynxflow/seo-engine 1.6.1 → 1.6.3
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/dist/free-public-tools.d.ts +74 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +388 -6
- package/dist/index.mjs +388 -6
- package/dist/matrix-engine.d.ts +3 -0
- package/dist/og-image-generator.d.ts +27 -0
- package/dist/ui-icons.d.ts +19 -0
- package/package.json +1 -1
- package/src/engine.test.ts +72 -0
- package/src/free-public-tools.ts +224 -0
- package/src/index.ts +3 -0
- package/src/llm-prompt.ts +18 -6
- package/src/matrix-engine.ts +42 -0
- package/src/og-image-generator.ts +109 -0
- package/src/ui-icons.ts +134 -0
package/dist/index.mjs
CHANGED
|
@@ -3231,6 +3231,87 @@ function resolveBuiltInLocations(territories) {
|
|
|
3231
3231
|
return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
3232
3232
|
}
|
|
3233
3233
|
|
|
3234
|
+
// src/og-image-generator.ts
|
|
3235
|
+
class OgImageGenerator {
|
|
3236
|
+
static generateOgImageUrl(domain, pageMeta, brandName) {
|
|
3237
|
+
const cleanDomain = domain.replace(/\/+$/, "");
|
|
3238
|
+
const params = new URLSearchParams({
|
|
3239
|
+
title: pageMeta.h1 || pageMeta.title,
|
|
3240
|
+
badge: pageMeta.matrixFamily.toUpperCase(),
|
|
3241
|
+
desc: pageMeta.description ? pageMeta.description.slice(0, 120) : "",
|
|
3242
|
+
brand: brandName
|
|
3243
|
+
});
|
|
3244
|
+
if (pageMeta.location) {
|
|
3245
|
+
params.set("loc", `${pageMeta.location.name}, ${pageMeta.location.country}`);
|
|
3246
|
+
}
|
|
3247
|
+
return `${cleanDomain}/api/og?${params.toString()}`;
|
|
3248
|
+
}
|
|
3249
|
+
static renderDynamicOgSvg(opts) {
|
|
3250
|
+
const brand = opts.brandName || "LynxSEO";
|
|
3251
|
+
const badge = opts.badgeText || "OFFICIAL";
|
|
3252
|
+
const title = opts.title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3253
|
+
const loc = opts.locationName ? `\uD83D\uDCCD ${opts.locationName}` : "⚡ Instant Cloud Setup";
|
|
3254
|
+
const rating = opts.ratingValue ?? 4.9;
|
|
3255
|
+
const reviews = opts.reviewCount ?? 1280;
|
|
3256
|
+
const accent = opts.brandColor || "#6366F1";
|
|
3257
|
+
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
|
3258
|
+
<defs>
|
|
3259
|
+
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
3260
|
+
<stop offset="0%" stop-color="#090D16"/>
|
|
3261
|
+
<stop offset="50%" stop-color="#0F172A"/>
|
|
3262
|
+
<stop offset="100%" stop-color="#020617"/>
|
|
3263
|
+
</linearGradient>
|
|
3264
|
+
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
3265
|
+
<stop offset="0%" stop-color="#FFFFFF"/>
|
|
3266
|
+
<stop offset="100%" stop-color="#CBD5E1"/>
|
|
3267
|
+
</linearGradient>
|
|
3268
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
3269
|
+
<feGaussianBlur stdDeviation="80" result="blur" />
|
|
3270
|
+
</filter>
|
|
3271
|
+
</defs>
|
|
3272
|
+
|
|
3273
|
+
<!-- Background -->
|
|
3274
|
+
<rect width="1200" height="630" fill="url(#bgGrad)"/>
|
|
3275
|
+
|
|
3276
|
+
<!-- Subtle Ambient Glow -->
|
|
3277
|
+
<circle cx="200" cy="150" r="220" fill="${accent}" opacity="0.15" filter="url(#glow)"/>
|
|
3278
|
+
<circle cx="1000" cy="480" r="260" fill="${accent}" opacity="0.12" filter="url(#glow)"/>
|
|
3279
|
+
|
|
3280
|
+
<!-- Top Glassmorphism Badge -->
|
|
3281
|
+
<g transform="translate(80, 80)">
|
|
3282
|
+
<rect width="220" height="42" rx="21" fill="${accent}" fill-opacity="0.15" stroke="${accent}" stroke-opacity="0.4" stroke-width="1.5"/>
|
|
3283
|
+
<text x="110" y="26" fill="#FFFFFF" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="14" font-weight="700" text-anchor="middle" letter-spacing="1.5">${badge}</text>
|
|
3284
|
+
</g>
|
|
3285
|
+
|
|
3286
|
+
<!-- Location / Scope Pill -->
|
|
3287
|
+
<g transform="translate(320, 80)">
|
|
3288
|
+
<rect width="260" height="42" rx="21" fill="#1E293B" fill-opacity="0.6" stroke="#334155" stroke-width="1"/>
|
|
3289
|
+
<text x="130" y="26" fill="#94A3B8" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="14" font-weight="600" text-anchor="middle">${loc}</text>
|
|
3290
|
+
</g>
|
|
3291
|
+
|
|
3292
|
+
<!-- Giant Title -->
|
|
3293
|
+
<text x="80" y="250" fill="url(#textGrad)" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="52" font-weight="900" letter-spacing="-1.5">
|
|
3294
|
+
<tspan x="80" dy="0">${title.slice(0, 38)}</tspan>
|
|
3295
|
+
${title.length > 38 ? `<tspan x="80" dy="68">${title.slice(38, 80)}</tspan>` : ""}
|
|
3296
|
+
</text>
|
|
3297
|
+
|
|
3298
|
+
<!-- Trust Stars & Social Proof -->
|
|
3299
|
+
<g transform="translate(80, 470)">
|
|
3300
|
+
<text x="0" y="32" fill="#FBBF24" font-size="24">★★★★★</text>
|
|
3301
|
+
<text x="140" y="30" fill="#F8FAFC" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="20" font-weight="800">${rating} / 5</text>
|
|
3302
|
+
<text x="210" y="30" fill="#64748B" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="500">(${reviews.toLocaleString()} verified reviews)</text>
|
|
3303
|
+
</g>
|
|
3304
|
+
|
|
3305
|
+
<!-- Footer Brand -->
|
|
3306
|
+
<g transform="translate(80, 540)">
|
|
3307
|
+
<line x1="0" y1="0" x2="1040" y2="0" stroke="#1E293B" stroke-width="1.5"/>
|
|
3308
|
+
<text x="0" y="42" fill="#FFFFFF" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="20" font-weight="800">${brand}</text>
|
|
3309
|
+
<text x="1040" y="42" fill="#64748B" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="500" text-anchor="end">Engineered with @lynxflow/seo-engine</text>
|
|
3310
|
+
</g>
|
|
3311
|
+
</svg>`;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3234
3315
|
// src/matrix-engine.ts
|
|
3235
3316
|
class PseoMatrixEngine {
|
|
3236
3317
|
legalEngine;
|
|
@@ -3734,6 +3815,43 @@ class PseoMatrixEngine {
|
|
|
3734
3815
|
}
|
|
3735
3816
|
if (data.calculators) {
|
|
3736
3817
|
allPages.push(...this.generateCalculatorMatrix(domain, data.calculators, options));
|
|
3818
|
+
} else if (options.enableFreeTools !== false) {
|
|
3819
|
+
const defaultCalculators = [
|
|
3820
|
+
{
|
|
3821
|
+
slug: "serp-preview-simulator",
|
|
3822
|
+
title: "Google SERP Snippet Preview & CTR Simulator",
|
|
3823
|
+
formulaDescription: "Simulate desktop and mobile Google search appearance and optimize CTR",
|
|
3824
|
+
savingsMetric: "Search Click-Through Rate",
|
|
3825
|
+
inputs: ["pageTitle", "metaDescription", "targetKeyword"]
|
|
3826
|
+
},
|
|
3827
|
+
{
|
|
3828
|
+
slug: "roi-calculator",
|
|
3829
|
+
title: "Automation ROI & Time-Saved Calculator",
|
|
3830
|
+
formulaDescription: "Calculate weekly operational hours recovered and monthly budget saved",
|
|
3831
|
+
savingsMetric: "Monthly Net Financial Savings",
|
|
3832
|
+
inputs: ["hoursSpentWeekly", "hourlyRate", "teamMembers"]
|
|
3833
|
+
},
|
|
3834
|
+
{
|
|
3835
|
+
slug: "seo-slug-generator",
|
|
3836
|
+
title: "SEO URL Slug Cleaner & Optimizer",
|
|
3837
|
+
formulaDescription: "Strip stop words, dates, and noise characters for high-CTR clean URLs",
|
|
3838
|
+
savingsMetric: "URL Cleanliness & Readability",
|
|
3839
|
+
inputs: ["rawTitle", "language"]
|
|
3840
|
+
},
|
|
3841
|
+
{
|
|
3842
|
+
slug: "keyword-density-checker",
|
|
3843
|
+
title: "Live Keyword Density & Readability Analyzer",
|
|
3844
|
+
formulaDescription: "Analyze n-gram frequency, reading time, and content depth",
|
|
3845
|
+
savingsMetric: "Content Optimization Score",
|
|
3846
|
+
inputs: ["articleText"]
|
|
3847
|
+
}
|
|
3848
|
+
];
|
|
3849
|
+
allPages.push(...this.generateCalculatorMatrix(domain, defaultCalculators, options));
|
|
3850
|
+
}
|
|
3851
|
+
for (const p of allPages) {
|
|
3852
|
+
if (!p.ogImageUrl) {
|
|
3853
|
+
p.ogImageUrl = OgImageGenerator.generateOgImageUrl(domain, p, options.brandName);
|
|
3854
|
+
}
|
|
3737
3855
|
}
|
|
3738
3856
|
return allPages;
|
|
3739
3857
|
}
|
|
@@ -3978,6 +4096,253 @@ function renderBrandIconSvg(input, className = "w-5 h-5 inline-block") {
|
|
|
3978
4096
|
return "";
|
|
3979
4097
|
return `<svg class="${className}" viewBox="${icon.viewBox}" fill="currentColor" aria-hidden="true"><path d="${icon.svgPath}"/></svg>`;
|
|
3980
4098
|
}
|
|
4099
|
+
// src/ui-icons.ts
|
|
4100
|
+
var UI_ICONS = {
|
|
4101
|
+
zap: {
|
|
4102
|
+
name: "Zap / Lightning",
|
|
4103
|
+
viewBox: "0 0 24 24",
|
|
4104
|
+
svgPath: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
4105
|
+
},
|
|
4106
|
+
shield: {
|
|
4107
|
+
name: "Shield / Security",
|
|
4108
|
+
viewBox: "0 0 24 24",
|
|
4109
|
+
svgPath: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
4110
|
+
},
|
|
4111
|
+
star: {
|
|
4112
|
+
name: "Star / Rating",
|
|
4113
|
+
viewBox: "0 0 24 24",
|
|
4114
|
+
svgPath: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" fill="currentColor"/>'
|
|
4115
|
+
},
|
|
4116
|
+
chart: {
|
|
4117
|
+
name: "Chart / Analytics",
|
|
4118
|
+
viewBox: "0 0 24 24",
|
|
4119
|
+
svgPath: '<line x1="18" y1="20" x2="18" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="12" y1="20" x2="12" y2="4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="6" y1="20" x2="6" y2="14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'
|
|
4120
|
+
},
|
|
4121
|
+
calendar: {
|
|
4122
|
+
name: "Calendar / Scheduling",
|
|
4123
|
+
viewBox: "0 0 24 24",
|
|
4124
|
+
svgPath: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>'
|
|
4125
|
+
},
|
|
4126
|
+
users: {
|
|
4127
|
+
name: "Users / Team",
|
|
4128
|
+
viewBox: "0 0 24 24",
|
|
4129
|
+
svgPath: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="9" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2"/><path d="M23 21v-2a4 4 0 0 0-3-3.87" fill="none" stroke="currentColor" stroke-width="2"/><path d="M16 3.13a4 4 0 0 1 0 7.75" fill="none" stroke="currentColor" stroke-width="2"/>'
|
|
4130
|
+
},
|
|
4131
|
+
rocket: {
|
|
4132
|
+
name: "Rocket / Speed",
|
|
4133
|
+
viewBox: "0 0 24 24",
|
|
4134
|
+
svgPath: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" fill="none" stroke="currentColor" stroke-width="2"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" fill="none" stroke="currentColor" stroke-width="2"/>'
|
|
4135
|
+
},
|
|
4136
|
+
sparkles: {
|
|
4137
|
+
name: "Sparkles / AI",
|
|
4138
|
+
viewBox: "0 0 24 24",
|
|
4139
|
+
svgPath: '<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3L12 3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>'
|
|
4140
|
+
},
|
|
4141
|
+
check: {
|
|
4142
|
+
name: "Check / Success",
|
|
4143
|
+
viewBox: "0 0 24 24",
|
|
4144
|
+
svgPath: '<polyline points="20 6 9 17 4 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
4145
|
+
},
|
|
4146
|
+
lock: {
|
|
4147
|
+
name: "Lock / Privacy",
|
|
4148
|
+
viewBox: "0 0 24 24",
|
|
4149
|
+
svgPath: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4" fill="none" stroke="currentColor" stroke-width="2"/>'
|
|
4150
|
+
},
|
|
4151
|
+
clock: {
|
|
4152
|
+
name: "Clock / 24-7",
|
|
4153
|
+
viewBox: "0 0 24 24",
|
|
4154
|
+
svgPath: '<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><polyline points="12 6 12 12 16 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'
|
|
4155
|
+
},
|
|
4156
|
+
phone: {
|
|
4157
|
+
name: "Phone / Contact",
|
|
4158
|
+
viewBox: "0 0 24 24",
|
|
4159
|
+
svgPath: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" fill="none" stroke="currentColor" stroke-width="2"/>'
|
|
4160
|
+
},
|
|
4161
|
+
mapPin: {
|
|
4162
|
+
name: "Map Pin / Local",
|
|
4163
|
+
viewBox: "0 0 24 24",
|
|
4164
|
+
svgPath: '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2"/>'
|
|
4165
|
+
},
|
|
4166
|
+
euro: {
|
|
4167
|
+
name: "Euro / Pricing",
|
|
4168
|
+
viewBox: "0 0 24 24",
|
|
4169
|
+
svgPath: '<path d="M4 10h12M4 14h9M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12a7.9 7.9 0 0 0 7.8 8 7.7 7.7 0 0 0 5.2-2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>'
|
|
4170
|
+
},
|
|
4171
|
+
arrowRight: {
|
|
4172
|
+
name: "Arrow Right",
|
|
4173
|
+
viewBox: "0 0 24 24",
|
|
4174
|
+
svgPath: '<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="12 5 19 12 12 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4177
|
+
function renderUiIconSvg(iconName, className = "w-5 h-5 inline-block") {
|
|
4178
|
+
const icon = UI_ICONS[iconName.toLowerCase()] || UI_ICONS.check;
|
|
4179
|
+
return `<svg class="${className}" viewBox="${icon.viewBox}" aria-hidden="true">${icon.svgPath}</svg>`;
|
|
4180
|
+
}
|
|
4181
|
+
function resolveFeatureIcon(featureText) {
|
|
4182
|
+
const lower = featureText.toLowerCase();
|
|
4183
|
+
if (lower.includes("secur") || lower.includes("rgpd") || lower.includes("gdpr") || lower.includes("protect") || lower.includes("bank")) {
|
|
4184
|
+
return "shield";
|
|
4185
|
+
}
|
|
4186
|
+
if (lower.includes("sync") || lower.includes("speed") || lower.includes("instant") || lower.includes("real-time") || lower.includes("fast") || lower.includes("rapide")) {
|
|
4187
|
+
return "zap";
|
|
4188
|
+
}
|
|
4189
|
+
if (lower.includes("ai") || lower.includes("ia") || lower.includes("auto") || lower.includes("smart") || lower.includes("intel")) {
|
|
4190
|
+
return "sparkles";
|
|
4191
|
+
}
|
|
4192
|
+
if (lower.includes("chart") || lower.includes("analyt") || lower.includes("report") || lower.includes("stat") || lower.includes("roi") || lower.includes("kpi")) {
|
|
4193
|
+
return "chart";
|
|
4194
|
+
}
|
|
4195
|
+
if (lower.includes("schedul") || lower.includes("calen") || lower.includes("plan") || lower.includes("agenda")) {
|
|
4196
|
+
return "calendar";
|
|
4197
|
+
}
|
|
4198
|
+
if (lower.includes("user") || lower.includes("team") || lower.includes("collab") || lower.includes("client") || lower.includes("contact")) {
|
|
4199
|
+
return "users";
|
|
4200
|
+
}
|
|
4201
|
+
if (lower.includes("price") || lower.includes("tarif") || lower.includes("cost") || lower.includes("devis") || lower.includes("factur")) {
|
|
4202
|
+
return "euro";
|
|
4203
|
+
}
|
|
4204
|
+
if (lower.includes("local") || lower.includes("city") || lower.includes("ville") || lower.includes("map") || lower.includes("gps")) {
|
|
4205
|
+
return "mapPin";
|
|
4206
|
+
}
|
|
4207
|
+
if (lower.includes("24/7") || lower.includes("support") || lower.includes("hour") || lower.includes("time") || lower.includes("temps")) {
|
|
4208
|
+
return "clock";
|
|
4209
|
+
}
|
|
4210
|
+
return "zap";
|
|
4211
|
+
}
|
|
4212
|
+
// src/free-public-tools.ts
|
|
4213
|
+
class FreePublicToolsEngine {
|
|
4214
|
+
static simulateSerpSnippet(input) {
|
|
4215
|
+
const title = (input.title || "").trim();
|
|
4216
|
+
const description = (input.description || "").trim();
|
|
4217
|
+
const url = (input.url || "https://example.com").trim();
|
|
4218
|
+
const kw = (input.targetKeyword || "").toLowerCase().trim();
|
|
4219
|
+
const titlePixelWidth = title.length * 9.5;
|
|
4220
|
+
const maxTitlePixels = 580;
|
|
4221
|
+
const maxDescChars = 158;
|
|
4222
|
+
const isTitleTruncated = titlePixelWidth > maxTitlePixels;
|
|
4223
|
+
const truncatedTitle = isTitleTruncated ? title.slice(0, 56) + "..." : title;
|
|
4224
|
+
const isDescriptionTruncated = description.length > maxDescChars;
|
|
4225
|
+
const truncatedDescription = isDescriptionTruncated ? description.slice(0, 155) + "..." : description;
|
|
4226
|
+
let ctrScore = 50;
|
|
4227
|
+
const recommendations = [];
|
|
4228
|
+
if (title.length >= 40 && title.length <= 60) {
|
|
4229
|
+
ctrScore += 20;
|
|
4230
|
+
} else if (title.length < 30) {
|
|
4231
|
+
recommendations.push("Le titre est trop court (< 30 car.). Ajoutez des précisions pour booster le clic.");
|
|
4232
|
+
} else if (isTitleTruncated) {
|
|
4233
|
+
ctrScore -= 10;
|
|
4234
|
+
recommendations.push("Le titre dépasse 580px et sera tronqué sur Google Desktop.");
|
|
4235
|
+
}
|
|
4236
|
+
if (kw && title.toLowerCase().includes(kw)) {
|
|
4237
|
+
ctrScore += 15;
|
|
4238
|
+
} else if (kw) {
|
|
4239
|
+
recommendations.push(`Le mot-clé principal "${kw}" est absent du titre.`);
|
|
4240
|
+
}
|
|
4241
|
+
if (description.length >= 120 && description.length <= 158) {
|
|
4242
|
+
ctrScore += 15;
|
|
4243
|
+
} else if (description.length < 80) {
|
|
4244
|
+
recommendations.push("La méta-description est trop courte (< 80 car.).");
|
|
4245
|
+
}
|
|
4246
|
+
return {
|
|
4247
|
+
title,
|
|
4248
|
+
truncatedTitle,
|
|
4249
|
+
titlePixelWidth: Math.round(titlePixelWidth),
|
|
4250
|
+
isTitleTruncated,
|
|
4251
|
+
description,
|
|
4252
|
+
truncatedDescription,
|
|
4253
|
+
isDescriptionTruncated,
|
|
4254
|
+
urlPreview: url,
|
|
4255
|
+
estimatedCtrScore: Math.min(Math.max(ctrScore, 10), 100),
|
|
4256
|
+
recommendations
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
static calculateRoi(input) {
|
|
4260
|
+
const hours = Math.max(input.hoursSpentPerWeek || 5, 1);
|
|
4261
|
+
const rate = Math.max(input.hourlyRateOrSalary || 35, 1);
|
|
4262
|
+
const team = Math.max(input.teamMembersCount || 1, 1);
|
|
4263
|
+
const softwareCost = input.softwareMonthlyPrice ?? 49;
|
|
4264
|
+
const hoursSavedWeekly = Math.round(hours * 0.75 * team * 10) / 10;
|
|
4265
|
+
const hoursSavedMonthly = Math.round(hoursSavedWeekly * 4.33);
|
|
4266
|
+
const monthlyGrossSavings = hoursSavedMonthly * rate;
|
|
4267
|
+
const monthlyNetSavings = Math.max(monthlyGrossSavings - softwareCost, 0);
|
|
4268
|
+
const annualCostSavings = monthlyNetSavings * 12;
|
|
4269
|
+
const roiPercentage = softwareCost > 0 ? Math.round(monthlyNetSavings / softwareCost * 100) : 1000;
|
|
4270
|
+
const breakEvenDays = monthlyGrossSavings > 0 ? Math.max(Math.round(softwareCost / (monthlyGrossSavings / 30)), 1) : 30;
|
|
4271
|
+
return {
|
|
4272
|
+
hoursSavedWeekly,
|
|
4273
|
+
hoursSavedMonthly,
|
|
4274
|
+
monthlyCostSavings: Math.round(monthlyNetSavings),
|
|
4275
|
+
annualCostSavings: Math.round(annualCostSavings),
|
|
4276
|
+
roiPercentage,
|
|
4277
|
+
breakEvenDays
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
static analyzeTextDensity(text, language = "fr") {
|
|
4281
|
+
if (!text || typeof text !== "string") {
|
|
4282
|
+
return { totalWords: 0, characterCount: 0, readingTimeMinutes: 0, topKeywords: [], seoReadabilityScore: 0 };
|
|
4283
|
+
}
|
|
4284
|
+
const clean = text.toLowerCase().replace(/[^a-z0-9à-ÿ\s]/g, " ");
|
|
4285
|
+
const words = clean.split(/\s+/).filter((w) => w.length > 2);
|
|
4286
|
+
const totalWords = words.length;
|
|
4287
|
+
const characterCount = text.length;
|
|
4288
|
+
const readingTimeMinutes = Math.max(Math.round(totalWords / 200 * 10) / 10, 0.5);
|
|
4289
|
+
const stopWords = new Set(["pour", "dans", "avec", "sur", "les", "des", "une", "que", "qui", "est", "par", "the", "and", "for", "with", "this", "that"]);
|
|
4290
|
+
const frequencyMap = new Map;
|
|
4291
|
+
for (const w of words) {
|
|
4292
|
+
if (!stopWords.has(w)) {
|
|
4293
|
+
frequencyMap.set(w, (frequencyMap.get(w) || 0) + 1);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
const sorted = Array.from(frequencyMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
4297
|
+
const topKeywords = sorted.slice(0, 8).map(([word, count]) => ({
|
|
4298
|
+
word,
|
|
4299
|
+
count,
|
|
4300
|
+
densityPercent: totalWords > 0 ? Math.round(count / totalWords * 1000) / 10 : 0
|
|
4301
|
+
}));
|
|
4302
|
+
let seoReadabilityScore = 70;
|
|
4303
|
+
if (totalWords >= 600)
|
|
4304
|
+
seoReadabilityScore += 20;
|
|
4305
|
+
else if (totalWords < 300)
|
|
4306
|
+
seoReadabilityScore -= 25;
|
|
4307
|
+
return {
|
|
4308
|
+
totalWords,
|
|
4309
|
+
characterCount,
|
|
4310
|
+
readingTimeMinutes,
|
|
4311
|
+
topKeywords,
|
|
4312
|
+
seoReadabilityScore: Math.min(Math.max(seoReadabilityScore, 10), 100)
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4315
|
+
static cleanSlugTool(input, lang = "fr") {
|
|
4316
|
+
const clean = cleanSeoSlug(input, { language: lang });
|
|
4317
|
+
return {
|
|
4318
|
+
original: input,
|
|
4319
|
+
cleanSlug: clean,
|
|
4320
|
+
charSavings: Math.max(input.length - clean.length, 0)
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
static generateSampleSchema(type, name, url) {
|
|
4324
|
+
if (type === "localBusiness") {
|
|
4325
|
+
return ExtendedSchemaGraphBuilder.buildLocalBusiness({
|
|
4326
|
+
name,
|
|
4327
|
+
url,
|
|
4328
|
+
city: "Paris",
|
|
4329
|
+
country: "France"
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
if (type === "faq") {
|
|
4333
|
+
return ExtendedSchemaGraphBuilder.buildFAQPage([
|
|
4334
|
+
{ question: `Comment fonctionne ${name} ?`, answer: `${name} automatise vos opérations en quelques clics.` },
|
|
4335
|
+
{ question: `Quel est le tarif de ${name} ?`, answer: `Un essai gratuit de 14 jours est disponible sans carte bancaire.` }
|
|
4336
|
+
]);
|
|
4337
|
+
}
|
|
4338
|
+
return ExtendedSchemaGraphBuilder.buildSoftwareApplication({
|
|
4339
|
+
name,
|
|
4340
|
+
description: `Solution logicielle professionnelle ${name}`,
|
|
4341
|
+
url,
|
|
4342
|
+
priceMonthly: 29
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
3981
4346
|
// src/urlytics-engine.ts
|
|
3982
4347
|
class UrlyticsEngine {
|
|
3983
4348
|
static parseUrl(rawUrl) {
|
|
@@ -4388,22 +4753,34 @@ The SDK equips applications with 9 enterprise-grade SEO engines:
|
|
|
4388
4753
|
4. **\uD83C\uDFA8 Vector Brand & Social Icons (\`renderBrandIconSvg\`):**
|
|
4389
4754
|
- Zero-dependency official SVG vectors for 30+ brands (Facebook, Instagram, LinkedIn, Shopify, Slack, WhatsApp, Google, GitHub, TikTok, YouTube).
|
|
4390
4755
|
|
|
4391
|
-
5.
|
|
4756
|
+
5. **✨ Standard UI & Feature Icons (\`renderUiIconSvg\`, \`resolveFeatureIcon\`):**
|
|
4757
|
+
- Zero-dependency Lucide-style vector icons (shield, zap, chart, calendar, users, rocket, sparkles, check, lock, clock, phone, mapPin, euro).
|
|
4758
|
+
- Semantic keyword auto-resolver: Automatically maps feature copy to the most relevant icon.
|
|
4759
|
+
|
|
4760
|
+
6. **\uD83D\uDDBC️ Dynamic OpenGraph (OG) Image Generator (\`OgImageGenerator\`):**
|
|
4761
|
+
- \`renderDynamicOgSvg(opts)\`: Generates high-converting 1200x630 SVG social cards with glassmorphism badges, star ratings, and gradient backdrops.
|
|
4762
|
+
- \`generateOgImageUrl(domain, pageMeta, brandName)\`: Edge-ready query URL for \`app/api/og/route.ts\`.
|
|
4763
|
+
|
|
4764
|
+
7. **\uD83D\uDEE0️ Free Public Interactive Tools & Lead Magnets (\`FreePublicToolsEngine\`):**
|
|
4765
|
+
- Ahrefs/Semrush-style high-intent public calculators: Google SERP Preview Simulator, Automation ROI Calculator, Keyword Density Analyzer, and Schema.org Generator.
|
|
4766
|
+
- Boosts dwell time, organic backlinks, and visitor-to-customer conversion.
|
|
4767
|
+
|
|
4768
|
+
8. **\uD83C\uDFDB️ Google Schema.org Rich Graphs (\`ExtendedSchemaGraphBuilder\`):**
|
|
4392
4769
|
- \`buildAggregateRating\` (Google Gold Stars 4.9★), \`buildAggregateOffer\` (price ranges), \`buildLocalBusiness\` (GPS & opening hours), \`buildSoftwareApplication\`, \`buildHowTo\`, \`buildFAQPage\`, \`buildBreadcrumbs\`.
|
|
4393
4770
|
|
|
4394
|
-
|
|
4771
|
+
9. **\uD83D\uDD0D URL Decomposition & Analysis (\`UrlyticsEngine\`):**
|
|
4395
4772
|
- Deep structural directory decomposition, depth tracking, query parameter analysis, and slug tokenization (inspired by advertools).
|
|
4396
4773
|
|
|
4397
|
-
|
|
4774
|
+
10. **⚡ SEM Keyword Combinator (\`KeywordPermutatorEngine\`):**
|
|
4398
4775
|
- Combinatorial matrix generation: Products × Modifiers × Locations with match types (\`[exact]\`, \`"phrase"\`, \`+broad\`) and intent classification.
|
|
4399
4776
|
|
|
4400
|
-
|
|
4777
|
+
11. **⚖️ Legal & Editorial Compliance (\`LegalDisclaimerEngine\`):**
|
|
4401
4778
|
- 7 SeedRank-grade disclaimer templates, 90-day staleness verification, and multi-language anti-disparagement guardian.
|
|
4402
4779
|
|
|
4403
|
-
|
|
4780
|
+
12. **\uD83D\uDCCA Telemetry & Cloud Metering (\`matrixEngine.getTelemetryPayload\` & \`LynxAnalyticsClient\`):**
|
|
4404
4781
|
- Synchronizes total programmatic pages, indexed hubs, and AI bot crawl hits (GPTBot, ClaudeBot, PerplexityBot) with the LynxSEO Studio dashboard.
|
|
4405
4782
|
|
|
4406
|
-
|
|
4783
|
+
13. **\uD83E\uDDF9 URL Slug Engine & Schema.org Graphs:**
|
|
4407
4784
|
- \`cleanSeoSlug(text, { language })\`: Strips accents, stop words across 10+ languages, and non-alphanumeric noise.
|
|
4408
4785
|
- Outputs Google-validated Schema.org JSON-LD graphs with 0 syntax errors.
|
|
4409
4786
|
`.trim();
|
|
@@ -6552,8 +6929,10 @@ var LynxSeo = {
|
|
|
6552
6929
|
var src_default = LynxSeo;
|
|
6553
6930
|
export {
|
|
6554
6931
|
validateSeoSlug,
|
|
6932
|
+
resolveFeatureIcon,
|
|
6555
6933
|
resolveBuiltInLocations,
|
|
6556
6934
|
resolveBrandIcon,
|
|
6935
|
+
renderUiIconSvg,
|
|
6557
6936
|
renderBrandIconSvg,
|
|
6558
6937
|
getSeoAgentPrompt,
|
|
6559
6938
|
src_default as default,
|
|
@@ -6561,6 +6940,7 @@ export {
|
|
|
6561
6940
|
cleanSeoSlug,
|
|
6562
6941
|
YoastParityEngine,
|
|
6563
6942
|
UrlyticsEngine,
|
|
6943
|
+
UI_ICONS,
|
|
6564
6944
|
TokenQuotaManager,
|
|
6565
6945
|
TechnicalRulesAuditor,
|
|
6566
6946
|
TeamRbacEngine,
|
|
@@ -6580,6 +6960,7 @@ export {
|
|
|
6580
6960
|
RankMathParityEngine,
|
|
6581
6961
|
PseoMatrixEngine,
|
|
6582
6962
|
PSEO_AGENT_SYSTEM_PROMPT,
|
|
6963
|
+
OgImageGenerator,
|
|
6583
6964
|
NGramDensityAnalyzer,
|
|
6584
6965
|
McpSeoServerHub,
|
|
6585
6966
|
MasterMarketingEngine,
|
|
@@ -6600,6 +6981,7 @@ export {
|
|
|
6600
6981
|
I18nDetector,
|
|
6601
6982
|
HyperswitchGateway,
|
|
6602
6983
|
GeoMeshLinkingEngine,
|
|
6984
|
+
FreePublicToolsEngine,
|
|
6603
6985
|
ExtendedSchemaGraphBuilder,
|
|
6604
6986
|
EmbeddableSeoWidgetGenerator,
|
|
6605
6987
|
DeepCrawlerAuditor,
|
package/dist/matrix-engine.d.ts
CHANGED
|
@@ -95,6 +95,7 @@ export interface PseoCalculator {
|
|
|
95
95
|
title: string;
|
|
96
96
|
formulaDescription: string;
|
|
97
97
|
savingsMetric: string;
|
|
98
|
+
inputs?: string[];
|
|
98
99
|
}
|
|
99
100
|
export interface SchemaOrgGraph {
|
|
100
101
|
"@context": "https://schema.org";
|
|
@@ -110,6 +111,7 @@ export interface GeneratedPageMeta {
|
|
|
110
111
|
robots: "index, follow" | "noindex, follow";
|
|
111
112
|
schemaGraph: SchemaOrgGraph;
|
|
112
113
|
disclaimerText?: string;
|
|
114
|
+
ogImageUrl?: string;
|
|
113
115
|
neighboringLinks?: {
|
|
114
116
|
name: string;
|
|
115
117
|
url: string;
|
|
@@ -131,6 +133,7 @@ export interface MatrixOptions {
|
|
|
131
133
|
countries?: string[] | string;
|
|
132
134
|
territories?: string[] | string;
|
|
133
135
|
cleanDirectRoutes?: boolean;
|
|
136
|
+
enableFreeTools?: boolean;
|
|
134
137
|
minPopulationToIndex?: number;
|
|
135
138
|
defaultCurrency?: string;
|
|
136
139
|
defaultCurrencySymbol?: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🖼️ Dynamic OpenGraph (OG) & Social Card Image Generator
|
|
3
|
+
* Generates high-converting 1200x630 SVG social preview cards and URL queries
|
|
4
|
+
* with zero heavy headless browser dependencies (Puppeteer/Playwright free).
|
|
5
|
+
*/
|
|
6
|
+
import { GeneratedPageMeta } from "./matrix-engine";
|
|
7
|
+
export interface OgImageOptions {
|
|
8
|
+
brandName: string;
|
|
9
|
+
title: string;
|
|
10
|
+
badgeText?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
locationName?: string;
|
|
13
|
+
ratingValue?: number;
|
|
14
|
+
reviewCount?: number;
|
|
15
|
+
brandColor?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class OgImageGenerator {
|
|
18
|
+
/**
|
|
19
|
+
* Generates a fully formatted URL for dynamic Edge / API OG image generation.
|
|
20
|
+
* e.g. /api/og?title=...&badge=...&city=...
|
|
21
|
+
*/
|
|
22
|
+
static generateOgImageUrl(domain: string, pageMeta: GeneratedPageMeta, brandName: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Generates an ultra-crisp 1200x630 pure SVG image string for social previews.
|
|
25
|
+
*/
|
|
26
|
+
static renderDynamicOgSvg(opts: OgImageOptions): string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🎨 Universal Zero-Dependency UI & Feature SVG Icons
|
|
3
|
+
* Provides crisp, modern Lucide-style vector icons for features, trust badges, and UI components.
|
|
4
|
+
* Includes automatic semantic keyword-to-icon detection for feature descriptions.
|
|
5
|
+
*/
|
|
6
|
+
export interface UiIcon {
|
|
7
|
+
name: string;
|
|
8
|
+
viewBox: string;
|
|
9
|
+
svgPath: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const UI_ICONS: Record<string, UiIcon>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns raw inline SVG markup for a standard UI icon.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderUiIconSvg(iconName: string, className?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Semantically resolves the most relevant UI icon based on feature text keywords.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveFeatureIcon(featureText: string): string;
|
package/package.json
CHANGED
package/src/engine.test.ts
CHANGED
|
@@ -8,6 +8,9 @@ import { UrlyticsEngine } from "./urlytics-engine";
|
|
|
8
8
|
import { KeywordPermutatorEngine } from "./keyword-permutator";
|
|
9
9
|
import { renderBrandIconSvg } from "./brand-icons";
|
|
10
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";
|
|
11
14
|
|
|
12
15
|
describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
|
|
13
16
|
it("English: Strips stop words and years", () => {
|
|
@@ -265,4 +268,73 @@ describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
|
|
|
265
268
|
expect(telemetry.brandName).toBe("Acme");
|
|
266
269
|
expect(telemetry.servicesCount).toBe(1);
|
|
267
270
|
});
|
|
271
|
+
|
|
272
|
+
it("UI Icons: Renders standard Lucide-style SVG and resolves semantic feature icons", () => {
|
|
273
|
+
const shieldSvg = renderUiIconSvg("shield");
|
|
274
|
+
expect(shieldSvg).toContain("<svg");
|
|
275
|
+
expect(shieldSvg).toContain("viewBox");
|
|
276
|
+
|
|
277
|
+
expect(resolveFeatureIcon("Bank-grade RGPD Security")).toBe("shield");
|
|
278
|
+
expect(resolveFeatureIcon("Real-time automated sync")).toBe("zap");
|
|
279
|
+
expect(resolveFeatureIcon("Analytics dashboard & KPI tracker")).toBe("chart");
|
|
280
|
+
expect(resolveFeatureIcon("Smart AI content generator")).toBe("sparkles");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("OgImageGenerator: Generates dynamic 1200x630 SVG and Edge URL query", () => {
|
|
284
|
+
const ogSvg = OgImageGenerator.renderDynamicOgSvg({
|
|
285
|
+
brandName: "Acme Cloud",
|
|
286
|
+
title: "Autopost Facebook & Instagram in Paris",
|
|
287
|
+
badgeText: "SOCIAL MEDIA",
|
|
288
|
+
locationName: "Paris, France",
|
|
289
|
+
ratingValue: 4.9,
|
|
290
|
+
reviewCount: 1280,
|
|
291
|
+
});
|
|
292
|
+
expect(ogSvg).toContain("<svg width=\"1200\" height=\"630\"");
|
|
293
|
+
expect(ogSvg).toContain("Autopost Facebook");
|
|
294
|
+
expect(ogSvg).toContain("4.9 / 5");
|
|
295
|
+
expect(ogSvg).toContain("Paris, France");
|
|
296
|
+
|
|
297
|
+
const engine = new PseoMatrixEngine();
|
|
298
|
+
const page = engine.resolvePage(
|
|
299
|
+
["autopost-facebook", "paris"],
|
|
300
|
+
"https://acme.com",
|
|
301
|
+
{
|
|
302
|
+
services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
|
|
303
|
+
},
|
|
304
|
+
{ brandName: "Acme", countries: ["france"] },
|
|
305
|
+
);
|
|
306
|
+
expect(page?.ogImageUrl).toBeDefined();
|
|
307
|
+
expect(page?.ogImageUrl).toContain("/api/og?");
|
|
308
|
+
expect(page?.ogImageUrl).toContain("brand=Acme");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("FreePublicToolsEngine: Simulates SERP, calculates ROI, and analyzes density", () => {
|
|
312
|
+
const serp = FreePublicToolsEngine.simulateSerpSnippet({
|
|
313
|
+
title: "Autopost Facebook & Instagram Tool 2026",
|
|
314
|
+
description: "Automate your social media scheduling across all major networks with AI copilot and analytics.",
|
|
315
|
+
url: "https://acme.com/autopost-facebook",
|
|
316
|
+
targetKeyword: "autopost",
|
|
317
|
+
});
|
|
318
|
+
expect(serp.estimatedCtrScore).toBeGreaterThan(50);
|
|
319
|
+
expect(serp.titlePixelWidth).toBeGreaterThan(0);
|
|
320
|
+
|
|
321
|
+
const roi = FreePublicToolsEngine.calculateRoi({
|
|
322
|
+
hoursSpentPerWeek: 10,
|
|
323
|
+
hourlyRateOrSalary: 50,
|
|
324
|
+
teamMembersCount: 2,
|
|
325
|
+
softwareMonthlyPrice: 79,
|
|
326
|
+
});
|
|
327
|
+
expect(roi.monthlyCostSavings).toBeGreaterThan(1000);
|
|
328
|
+
expect(roi.roiPercentage).toBeGreaterThan(100);
|
|
329
|
+
|
|
330
|
+
const density = FreePublicToolsEngine.analyzeTextDensity(
|
|
331
|
+
"Autopost Facebook automatise vos réseaux sociaux. Notre outil autopost vous fait gagner du temps avec autopost.",
|
|
332
|
+
"fr",
|
|
333
|
+
);
|
|
334
|
+
expect(density.totalWords).toBeGreaterThan(5);
|
|
335
|
+
expect(density.topKeywords[0].word).toBe("autopost");
|
|
336
|
+
|
|
337
|
+
const schema = FreePublicToolsEngine.generateSampleSchema("softwareApp", "Acme", "https://acme.com");
|
|
338
|
+
expect(schema["@type"]).toBe("SoftwareApplication");
|
|
339
|
+
});
|
|
268
340
|
});
|