@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🛠️ Free Public Interactive Tools & Lead Magnets Suite
|
|
3
|
+
* Inspired by Ahrefs, Semrush, Moz, and WordStream free tool ecosystems.
|
|
4
|
+
*
|
|
5
|
+
* Provides 6 zero-dependency interactive calculators and simulators that can be embedded
|
|
6
|
+
* on public programmatic pages to maximize dwell time, engagement, backlink acquisition, and user conversion.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cleanSeoSlug } from "./slug-engine";
|
|
10
|
+
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
11
|
+
|
|
12
|
+
export interface SerpSimulatorResult {
|
|
13
|
+
title: string;
|
|
14
|
+
truncatedTitle: string;
|
|
15
|
+
titlePixelWidth: number;
|
|
16
|
+
isTitleTruncated: boolean;
|
|
17
|
+
description: string;
|
|
18
|
+
truncatedDescription: string;
|
|
19
|
+
isDescriptionTruncated: boolean;
|
|
20
|
+
urlPreview: string;
|
|
21
|
+
estimatedCtrScore: number; // 0 to 100
|
|
22
|
+
recommendations: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RoiCalculatorResult {
|
|
26
|
+
hoursSavedWeekly: number;
|
|
27
|
+
hoursSavedMonthly: number;
|
|
28
|
+
monthlyCostSavings: number;
|
|
29
|
+
annualCostSavings: number;
|
|
30
|
+
roiPercentage: number;
|
|
31
|
+
breakEvenDays: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DensityAnalysisResult {
|
|
35
|
+
totalWords: number;
|
|
36
|
+
characterCount: number;
|
|
37
|
+
readingTimeMinutes: number;
|
|
38
|
+
topKeywords: { word: string; count: number; densityPercent: number }[];
|
|
39
|
+
seoReadabilityScore: number; // 0 to 100
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class FreePublicToolsEngine {
|
|
43
|
+
/**
|
|
44
|
+
* 1. Google SERP Preview & CTR Optimizer Simulator (Ahrefs / Moz Parity)
|
|
45
|
+
*/
|
|
46
|
+
static simulateSerpSnippet(input: {
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
url: string;
|
|
50
|
+
targetKeyword?: string;
|
|
51
|
+
}): SerpSimulatorResult {
|
|
52
|
+
const title = (input.title || "").trim();
|
|
53
|
+
const description = (input.description || "").trim();
|
|
54
|
+
const url = (input.url || "https://example.com").trim();
|
|
55
|
+
const kw = (input.targetKeyword || "").toLowerCase().trim();
|
|
56
|
+
|
|
57
|
+
// Average pixel width estimation (approx 10px per char on Arial 20px)
|
|
58
|
+
const titlePixelWidth = title.length * 9.5;
|
|
59
|
+
const maxTitlePixels = 580; // Google Desktop breakpoint
|
|
60
|
+
const maxDescChars = 158;
|
|
61
|
+
|
|
62
|
+
const isTitleTruncated = titlePixelWidth > maxTitlePixels;
|
|
63
|
+
const truncatedTitle = isTitleTruncated ? title.slice(0, 56) + "..." : title;
|
|
64
|
+
|
|
65
|
+
const isDescriptionTruncated = description.length > maxDescChars;
|
|
66
|
+
const truncatedDescription = isDescriptionTruncated ? description.slice(0, 155) + "..." : description;
|
|
67
|
+
|
|
68
|
+
let ctrScore = 50;
|
|
69
|
+
const recommendations: string[] = [];
|
|
70
|
+
|
|
71
|
+
// Title checks
|
|
72
|
+
if (title.length >= 40 && title.length <= 60) {
|
|
73
|
+
ctrScore += 20;
|
|
74
|
+
} else if (title.length < 30) {
|
|
75
|
+
recommendations.push("Le titre est trop court (< 30 car.). Ajoutez des précisions pour booster le clic.");
|
|
76
|
+
} else if (isTitleTruncated) {
|
|
77
|
+
ctrScore -= 10;
|
|
78
|
+
recommendations.push("Le titre dépasse 580px et sera tronqué sur Google Desktop.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Keyword presence
|
|
82
|
+
if (kw && title.toLowerCase().includes(kw)) {
|
|
83
|
+
ctrScore += 15;
|
|
84
|
+
} else if (kw) {
|
|
85
|
+
recommendations.push(`Le mot-clé principal "${kw}" est absent du titre.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Description checks
|
|
89
|
+
if (description.length >= 120 && description.length <= 158) {
|
|
90
|
+
ctrScore += 15;
|
|
91
|
+
} else if (description.length < 80) {
|
|
92
|
+
recommendations.push("La méta-description est trop courte (< 80 car.).");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
title,
|
|
97
|
+
truncatedTitle,
|
|
98
|
+
titlePixelWidth: Math.round(titlePixelWidth),
|
|
99
|
+
isTitleTruncated,
|
|
100
|
+
description,
|
|
101
|
+
truncatedDescription,
|
|
102
|
+
isDescriptionTruncated,
|
|
103
|
+
urlPreview: url,
|
|
104
|
+
estimatedCtrScore: Math.min(Math.max(ctrScore, 10), 100),
|
|
105
|
+
recommendations,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 2. Free ROI & Time-Saved Automation Calculator
|
|
111
|
+
*/
|
|
112
|
+
static calculateRoi(input: {
|
|
113
|
+
hoursSpentPerWeek: number;
|
|
114
|
+
hourlyRateOrSalary: number;
|
|
115
|
+
teamMembersCount?: number;
|
|
116
|
+
softwareMonthlyPrice?: number;
|
|
117
|
+
}): RoiCalculatorResult {
|
|
118
|
+
const hours = Math.max(input.hoursSpentPerWeek || 5, 1);
|
|
119
|
+
const rate = Math.max(input.hourlyRateOrSalary || 35, 1);
|
|
120
|
+
const team = Math.max(input.teamMembersCount || 1, 1);
|
|
121
|
+
const softwareCost = input.softwareMonthlyPrice ?? 49;
|
|
122
|
+
|
|
123
|
+
// Assuming automation saves ~75% of manual operational time
|
|
124
|
+
const hoursSavedWeekly = Math.round(hours * 0.75 * team * 10) / 10;
|
|
125
|
+
const hoursSavedMonthly = Math.round(hoursSavedWeekly * 4.33);
|
|
126
|
+
|
|
127
|
+
const monthlyGrossSavings = hoursSavedMonthly * rate;
|
|
128
|
+
const monthlyNetSavings = Math.max(monthlyGrossSavings - softwareCost, 0);
|
|
129
|
+
const annualCostSavings = monthlyNetSavings * 12;
|
|
130
|
+
|
|
131
|
+
const roiPercentage = softwareCost > 0 ? Math.round((monthlyNetSavings / softwareCost) * 100) : 1000;
|
|
132
|
+
const breakEvenDays = monthlyGrossSavings > 0 ? Math.max(Math.round((softwareCost / (monthlyGrossSavings / 30))), 1) : 30;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
hoursSavedWeekly,
|
|
136
|
+
hoursSavedMonthly,
|
|
137
|
+
monthlyCostSavings: Math.round(monthlyNetSavings),
|
|
138
|
+
annualCostSavings: Math.round(annualCostSavings),
|
|
139
|
+
roiPercentage,
|
|
140
|
+
breakEvenDays,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 3. Instant Keyword Density & Readability Analyzer
|
|
146
|
+
*/
|
|
147
|
+
static analyzeTextDensity(text: string, language: string = "fr"): DensityAnalysisResult {
|
|
148
|
+
if (!text || typeof text !== "string") {
|
|
149
|
+
return { totalWords: 0, characterCount: 0, readingTimeMinutes: 0, topKeywords: [], seoReadabilityScore: 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const clean = text.toLowerCase().replace(/[^a-z0-9à-ÿ\s]/g, " ");
|
|
153
|
+
const words = clean.split(/\s+/).filter((w) => w.length > 2);
|
|
154
|
+
const totalWords = words.length;
|
|
155
|
+
const characterCount = text.length;
|
|
156
|
+
const readingTimeMinutes = Math.max(Math.round((totalWords / 200) * 10) / 10, 0.5);
|
|
157
|
+
|
|
158
|
+
const stopWords = new Set(["pour", "dans", "avec", "sur", "les", "des", "une", "que", "qui", "est", "par", "the", "and", "for", "with", "this", "that"]);
|
|
159
|
+
const frequencyMap = new Map<string, number>();
|
|
160
|
+
|
|
161
|
+
for (const w of words) {
|
|
162
|
+
if (!stopWords.has(w)) {
|
|
163
|
+
frequencyMap.set(w, (frequencyMap.get(w) || 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sorted = Array.from(frequencyMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
168
|
+
const topKeywords = sorted.slice(0, 8).map(([word, count]) => ({
|
|
169
|
+
word,
|
|
170
|
+
count,
|
|
171
|
+
densityPercent: totalWords > 0 ? Math.round((count / totalWords) * 1000) / 10 : 0,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
let seoReadabilityScore = 70;
|
|
175
|
+
if (totalWords >= 600) seoReadabilityScore += 20;
|
|
176
|
+
else if (totalWords < 300) seoReadabilityScore -= 25;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
totalWords,
|
|
180
|
+
characterCount,
|
|
181
|
+
readingTimeMinutes,
|
|
182
|
+
topKeywords,
|
|
183
|
+
seoReadabilityScore: Math.min(Math.max(seoReadabilityScore, 10), 100),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 4. Free SEO Slug Cleanser Helper
|
|
189
|
+
*/
|
|
190
|
+
static cleanSlugTool(input: string, lang: string = "fr"): { original: string; cleanSlug: string; charSavings: number } {
|
|
191
|
+
const clean = cleanSeoSlug(input, { language: lang });
|
|
192
|
+
return {
|
|
193
|
+
original: input,
|
|
194
|
+
cleanSlug: clean,
|
|
195
|
+
charSavings: Math.max(input.length - clean.length, 0),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 5. Free Schema.org Generator Helper
|
|
201
|
+
*/
|
|
202
|
+
static generateSampleSchema(type: "localBusiness" | "softwareApp" | "faq", name: string, url: string): Record<string, unknown> {
|
|
203
|
+
if (type === "localBusiness") {
|
|
204
|
+
return ExtendedSchemaGraphBuilder.buildLocalBusiness({
|
|
205
|
+
name,
|
|
206
|
+
url,
|
|
207
|
+
city: "Paris",
|
|
208
|
+
country: "France",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (type === "faq") {
|
|
212
|
+
return ExtendedSchemaGraphBuilder.buildFAQPage([
|
|
213
|
+
{ question: `Comment fonctionne ${name} ?`, answer: `${name} automatise vos opérations en quelques clics.` },
|
|
214
|
+
{ question: `Quel est le tarif de ${name} ?`, answer: `Un essai gratuit de 14 jours est disponible sans carte bancaire.` },
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
return ExtendedSchemaGraphBuilder.buildSoftwareApplication({
|
|
218
|
+
name,
|
|
219
|
+
description: `Solution logicielle professionnelle ${name}`,
|
|
220
|
+
url,
|
|
221
|
+
priceMonthly: 29,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ export * from "./matrix-engine";
|
|
|
17
17
|
export * from "./built-in-locations";
|
|
18
18
|
export * from "./i18n-detector";
|
|
19
19
|
export * from "./brand-icons";
|
|
20
|
+
export * from "./ui-icons";
|
|
21
|
+
export * from "./og-image-generator";
|
|
22
|
+
export * from "./free-public-tools";
|
|
20
23
|
export * from "./urlytics-engine";
|
|
21
24
|
export * from "./keyword-permutator";
|
|
22
25
|
export * from "./llm-prompt";
|
package/src/llm-prompt.ts
CHANGED
|
@@ -275,22 +275,34 @@ The SDK equips applications with 9 enterprise-grade SEO engines:
|
|
|
275
275
|
4. **🎨 Vector Brand & Social Icons (\`renderBrandIconSvg\`):**
|
|
276
276
|
- Zero-dependency official SVG vectors for 30+ brands (Facebook, Instagram, LinkedIn, Shopify, Slack, WhatsApp, Google, GitHub, TikTok, YouTube).
|
|
277
277
|
|
|
278
|
-
5.
|
|
278
|
+
5. **✨ Standard UI & Feature Icons (\`renderUiIconSvg\`, \`resolveFeatureIcon\`):**
|
|
279
|
+
- Zero-dependency Lucide-style vector icons (shield, zap, chart, calendar, users, rocket, sparkles, check, lock, clock, phone, mapPin, euro).
|
|
280
|
+
- Semantic keyword auto-resolver: Automatically maps feature copy to the most relevant icon.
|
|
281
|
+
|
|
282
|
+
6. **🖼️ Dynamic OpenGraph (OG) Image Generator (\`OgImageGenerator\`):**
|
|
283
|
+
- \`renderDynamicOgSvg(opts)\`: Generates high-converting 1200x630 SVG social cards with glassmorphism badges, star ratings, and gradient backdrops.
|
|
284
|
+
- \`generateOgImageUrl(domain, pageMeta, brandName)\`: Edge-ready query URL for \`app/api/og/route.ts\`.
|
|
285
|
+
|
|
286
|
+
7. **🛠️ Free Public Interactive Tools & Lead Magnets (\`FreePublicToolsEngine\`):**
|
|
287
|
+
- Ahrefs/Semrush-style high-intent public calculators: Google SERP Preview Simulator, Automation ROI Calculator, Keyword Density Analyzer, and Schema.org Generator.
|
|
288
|
+
- Boosts dwell time, organic backlinks, and visitor-to-customer conversion.
|
|
289
|
+
|
|
290
|
+
8. **🏛️ Google Schema.org Rich Graphs (\`ExtendedSchemaGraphBuilder\`):**
|
|
279
291
|
- \`buildAggregateRating\` (Google Gold Stars 4.9★), \`buildAggregateOffer\` (price ranges), \`buildLocalBusiness\` (GPS & opening hours), \`buildSoftwareApplication\`, \`buildHowTo\`, \`buildFAQPage\`, \`buildBreadcrumbs\`.
|
|
280
292
|
|
|
281
|
-
|
|
293
|
+
9. **🔍 URL Decomposition & Analysis (\`UrlyticsEngine\`):**
|
|
282
294
|
- Deep structural directory decomposition, depth tracking, query parameter analysis, and slug tokenization (inspired by advertools).
|
|
283
295
|
|
|
284
|
-
|
|
296
|
+
10. **⚡ SEM Keyword Combinator (\`KeywordPermutatorEngine\`):**
|
|
285
297
|
- Combinatorial matrix generation: Products × Modifiers × Locations with match types (\`[exact]\`, \`"phrase"\`, \`+broad\`) and intent classification.
|
|
286
298
|
|
|
287
|
-
|
|
299
|
+
11. **⚖️ Legal & Editorial Compliance (\`LegalDisclaimerEngine\`):**
|
|
288
300
|
- 7 SeedRank-grade disclaimer templates, 90-day staleness verification, and multi-language anti-disparagement guardian.
|
|
289
301
|
|
|
290
|
-
|
|
302
|
+
12. **📊 Telemetry & Cloud Metering (\`matrixEngine.getTelemetryPayload\` & \`LynxAnalyticsClient\`):**
|
|
291
303
|
- Synchronizes total programmatic pages, indexed hubs, and AI bot crawl hits (GPTBot, ClaudeBot, PerplexityBot) with the LynxSEO Studio dashboard.
|
|
292
304
|
|
|
293
|
-
|
|
305
|
+
13. **🧹 URL Slug Engine & Schema.org Graphs:**
|
|
294
306
|
- \`cleanSeoSlug(text, { language })\`: Strips accents, stop words across 10+ languages, and non-alphanumeric noise.
|
|
295
307
|
- Outputs Google-validated Schema.org JSON-LD graphs with 0 syntax errors.
|
|
296
308
|
`.trim();
|
package/src/matrix-engine.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { cleanSeoSlug } from "./slug-engine";
|
|
|
14
14
|
import { LegalDisclaimerEngine } from "./legal-disclaimers";
|
|
15
15
|
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
16
16
|
import { resolveBuiltInLocations } from "./built-in-locations";
|
|
17
|
+
import { OgImageGenerator } from "./og-image-generator";
|
|
17
18
|
|
|
18
19
|
export type MatrixFamily =
|
|
19
20
|
| "local-geo"
|
|
@@ -124,6 +125,7 @@ export interface PseoCalculator {
|
|
|
124
125
|
title: string;
|
|
125
126
|
formulaDescription: string;
|
|
126
127
|
savingsMetric: string;
|
|
128
|
+
inputs?: string[];
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export interface SchemaOrgGraph {
|
|
@@ -141,6 +143,7 @@ export interface GeneratedPageMeta {
|
|
|
141
143
|
robots: "index, follow" | "noindex, follow";
|
|
142
144
|
schemaGraph: SchemaOrgGraph;
|
|
143
145
|
disclaimerText?: string;
|
|
146
|
+
ogImageUrl?: string;
|
|
144
147
|
neighboringLinks?: { name: string; url: string }[];
|
|
145
148
|
service?: PseoService;
|
|
146
149
|
location?: PseoLocation;
|
|
@@ -160,6 +163,7 @@ export interface MatrixOptions {
|
|
|
160
163
|
countries?: string[] | string; // e.g. ["france", "spain"] or "europe" or "international"
|
|
161
164
|
territories?: string[] | string; // Alias for countries
|
|
162
165
|
cleanDirectRoutes?: boolean; // When true (default), eliminates parasite words (/solutions/, etc.) for direct /{service}/{city}
|
|
166
|
+
enableFreeTools?: boolean; // When true (default), auto-generates 5 high-converting interactive tools like Ahrefs
|
|
163
167
|
minPopulationToIndex?: number;
|
|
164
168
|
defaultCurrency?: string;
|
|
165
169
|
defaultCurrencySymbol?: string;
|
|
@@ -822,6 +826,44 @@ export class PseoMatrixEngine {
|
|
|
822
826
|
}
|
|
823
827
|
if (data.calculators) {
|
|
824
828
|
allPages.push(...this.generateCalculatorMatrix(domain, data.calculators, options));
|
|
829
|
+
} else if (options.enableFreeTools !== false) {
|
|
830
|
+
const defaultCalculators: PseoCalculator[] = [
|
|
831
|
+
{
|
|
832
|
+
slug: "serp-preview-simulator",
|
|
833
|
+
title: "Google SERP Snippet Preview & CTR Simulator",
|
|
834
|
+
formulaDescription: "Simulate desktop and mobile Google search appearance and optimize CTR",
|
|
835
|
+
savingsMetric: "Search Click-Through Rate",
|
|
836
|
+
inputs: ["pageTitle", "metaDescription", "targetKeyword"],
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
slug: "roi-calculator",
|
|
840
|
+
title: "Automation ROI & Time-Saved Calculator",
|
|
841
|
+
formulaDescription: "Calculate weekly operational hours recovered and monthly budget saved",
|
|
842
|
+
savingsMetric: "Monthly Net Financial Savings",
|
|
843
|
+
inputs: ["hoursSpentWeekly", "hourlyRate", "teamMembers"],
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
slug: "seo-slug-generator",
|
|
847
|
+
title: "SEO URL Slug Cleaner & Optimizer",
|
|
848
|
+
formulaDescription: "Strip stop words, dates, and noise characters for high-CTR clean URLs",
|
|
849
|
+
savingsMetric: "URL Cleanliness & Readability",
|
|
850
|
+
inputs: ["rawTitle", "language"],
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
slug: "keyword-density-checker",
|
|
854
|
+
title: "Live Keyword Density & Readability Analyzer",
|
|
855
|
+
formulaDescription: "Analyze n-gram frequency, reading time, and content depth",
|
|
856
|
+
savingsMetric: "Content Optimization Score",
|
|
857
|
+
inputs: ["articleText"],
|
|
858
|
+
},
|
|
859
|
+
];
|
|
860
|
+
allPages.push(...this.generateCalculatorMatrix(domain, defaultCalculators, options));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (const p of allPages) {
|
|
864
|
+
if (!p.ogImageUrl) {
|
|
865
|
+
p.ogImageUrl = OgImageGenerator.generateOgImageUrl(domain, p, options.brandName);
|
|
866
|
+
}
|
|
825
867
|
}
|
|
826
868
|
|
|
827
869
|
return allPages;
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
7
|
+
import { GeneratedPageMeta } from "./matrix-engine";
|
|
8
|
+
|
|
9
|
+
export interface OgImageOptions {
|
|
10
|
+
brandName: string;
|
|
11
|
+
title: string;
|
|
12
|
+
badgeText?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
locationName?: string;
|
|
15
|
+
ratingValue?: number;
|
|
16
|
+
reviewCount?: number;
|
|
17
|
+
brandColor?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class OgImageGenerator {
|
|
21
|
+
/**
|
|
22
|
+
* Generates a fully formatted URL for dynamic Edge / API OG image generation.
|
|
23
|
+
* e.g. /api/og?title=...&badge=...&city=...
|
|
24
|
+
*/
|
|
25
|
+
static generateOgImageUrl(domain: string, pageMeta: GeneratedPageMeta, brandName: string): string {
|
|
26
|
+
const cleanDomain = domain.replace(/\/+$/, "");
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
title: pageMeta.h1 || pageMeta.title,
|
|
29
|
+
badge: pageMeta.matrixFamily.toUpperCase(),
|
|
30
|
+
desc: pageMeta.description ? pageMeta.description.slice(0, 120) : "",
|
|
31
|
+
brand: brandName,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (pageMeta.location) {
|
|
35
|
+
params.set("loc", `${pageMeta.location.name}, ${pageMeta.location.country}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${cleanDomain}/api/og?${params.toString()}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates an ultra-crisp 1200x630 pure SVG image string for social previews.
|
|
43
|
+
*/
|
|
44
|
+
static renderDynamicOgSvg(opts: OgImageOptions): string {
|
|
45
|
+
const brand = opts.brandName || "LynxSEO";
|
|
46
|
+
const badge = opts.badgeText || "OFFICIAL";
|
|
47
|
+
const title = opts.title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
|
+
const loc = opts.locationName ? `📍 ${opts.locationName}` : "⚡ Instant Cloud Setup";
|
|
49
|
+
const rating = opts.ratingValue ?? 4.9;
|
|
50
|
+
const reviews = opts.reviewCount ?? 1280;
|
|
51
|
+
const accent = opts.brandColor || "#6366F1";
|
|
52
|
+
|
|
53
|
+
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
|
54
|
+
<defs>
|
|
55
|
+
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
56
|
+
<stop offset="0%" stop-color="#090D16"/>
|
|
57
|
+
<stop offset="50%" stop-color="#0F172A"/>
|
|
58
|
+
<stop offset="100%" stop-color="#020617"/>
|
|
59
|
+
</linearGradient>
|
|
60
|
+
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
61
|
+
<stop offset="0%" stop-color="#FFFFFF"/>
|
|
62
|
+
<stop offset="100%" stop-color="#CBD5E1"/>
|
|
63
|
+
</linearGradient>
|
|
64
|
+
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
65
|
+
<feGaussianBlur stdDeviation="80" result="blur" />
|
|
66
|
+
</filter>
|
|
67
|
+
</defs>
|
|
68
|
+
|
|
69
|
+
<!-- Background -->
|
|
70
|
+
<rect width="1200" height="630" fill="url(#bgGrad)"/>
|
|
71
|
+
|
|
72
|
+
<!-- Subtle Ambient Glow -->
|
|
73
|
+
<circle cx="200" cy="150" r="220" fill="${accent}" opacity="0.15" filter="url(#glow)"/>
|
|
74
|
+
<circle cx="1000" cy="480" r="260" fill="${accent}" opacity="0.12" filter="url(#glow)"/>
|
|
75
|
+
|
|
76
|
+
<!-- Top Glassmorphism Badge -->
|
|
77
|
+
<g transform="translate(80, 80)">
|
|
78
|
+
<rect width="220" height="42" rx="21" fill="${accent}" fill-opacity="0.15" stroke="${accent}" stroke-opacity="0.4" stroke-width="1.5"/>
|
|
79
|
+
<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>
|
|
80
|
+
</g>
|
|
81
|
+
|
|
82
|
+
<!-- Location / Scope Pill -->
|
|
83
|
+
<g transform="translate(320, 80)">
|
|
84
|
+
<rect width="260" height="42" rx="21" fill="#1E293B" fill-opacity="0.6" stroke="#334155" stroke-width="1"/>
|
|
85
|
+
<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>
|
|
86
|
+
</g>
|
|
87
|
+
|
|
88
|
+
<!-- Giant Title -->
|
|
89
|
+
<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">
|
|
90
|
+
<tspan x="80" dy="0">${title.slice(0, 38)}</tspan>
|
|
91
|
+
${title.length > 38 ? `<tspan x="80" dy="68">${title.slice(38, 80)}</tspan>` : ""}
|
|
92
|
+
</text>
|
|
93
|
+
|
|
94
|
+
<!-- Trust Stars & Social Proof -->
|
|
95
|
+
<g transform="translate(80, 470)">
|
|
96
|
+
<text x="0" y="32" fill="#FBBF24" font-size="24">★★★★★</text>
|
|
97
|
+
<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>
|
|
98
|
+
<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>
|
|
99
|
+
</g>
|
|
100
|
+
|
|
101
|
+
<!-- Footer Brand -->
|
|
102
|
+
<g transform="translate(80, 540)">
|
|
103
|
+
<line x1="0" y1="0" x2="1040" y2="0" stroke="#1E293B" stroke-width="1.5"/>
|
|
104
|
+
<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>
|
|
105
|
+
<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>
|
|
106
|
+
</g>
|
|
107
|
+
</svg>`;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/ui-icons.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
|
|
7
|
+
export interface UiIcon {
|
|
8
|
+
name: string;
|
|
9
|
+
viewBox: string;
|
|
10
|
+
svgPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const UI_ICONS: Record<string, UiIcon> = {
|
|
14
|
+
zap: {
|
|
15
|
+
name: "Zap / Lightning",
|
|
16
|
+
viewBox: "0 0 24 24",
|
|
17
|
+
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"/>',
|
|
18
|
+
},
|
|
19
|
+
shield: {
|
|
20
|
+
name: "Shield / Security",
|
|
21
|
+
viewBox: "0 0 24 24",
|
|
22
|
+
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"/>',
|
|
23
|
+
},
|
|
24
|
+
star: {
|
|
25
|
+
name: "Star / Rating",
|
|
26
|
+
viewBox: "0 0 24 24",
|
|
27
|
+
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"/>',
|
|
28
|
+
},
|
|
29
|
+
chart: {
|
|
30
|
+
name: "Chart / Analytics",
|
|
31
|
+
viewBox: "0 0 24 24",
|
|
32
|
+
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"/>',
|
|
33
|
+
},
|
|
34
|
+
calendar: {
|
|
35
|
+
name: "Calendar / Scheduling",
|
|
36
|
+
viewBox: "0 0 24 24",
|
|
37
|
+
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"/>',
|
|
38
|
+
},
|
|
39
|
+
users: {
|
|
40
|
+
name: "Users / Team",
|
|
41
|
+
viewBox: "0 0 24 24",
|
|
42
|
+
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"/>',
|
|
43
|
+
},
|
|
44
|
+
rocket: {
|
|
45
|
+
name: "Rocket / Speed",
|
|
46
|
+
viewBox: "0 0 24 24",
|
|
47
|
+
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"/>',
|
|
48
|
+
},
|
|
49
|
+
sparkles: {
|
|
50
|
+
name: "Sparkles / AI",
|
|
51
|
+
viewBox: "0 0 24 24",
|
|
52
|
+
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"/>',
|
|
53
|
+
},
|
|
54
|
+
check: {
|
|
55
|
+
name: "Check / Success",
|
|
56
|
+
viewBox: "0 0 24 24",
|
|
57
|
+
svgPath: '<polyline points="20 6 9 17 4 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
58
|
+
},
|
|
59
|
+
lock: {
|
|
60
|
+
name: "Lock / Privacy",
|
|
61
|
+
viewBox: "0 0 24 24",
|
|
62
|
+
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"/>',
|
|
63
|
+
},
|
|
64
|
+
clock: {
|
|
65
|
+
name: "Clock / 24-7",
|
|
66
|
+
viewBox: "0 0 24 24",
|
|
67
|
+
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"/>',
|
|
68
|
+
},
|
|
69
|
+
phone: {
|
|
70
|
+
name: "Phone / Contact",
|
|
71
|
+
viewBox: "0 0 24 24",
|
|
72
|
+
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"/>',
|
|
73
|
+
},
|
|
74
|
+
mapPin: {
|
|
75
|
+
name: "Map Pin / Local",
|
|
76
|
+
viewBox: "0 0 24 24",
|
|
77
|
+
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"/>',
|
|
78
|
+
},
|
|
79
|
+
euro: {
|
|
80
|
+
name: "Euro / Pricing",
|
|
81
|
+
viewBox: "0 0 24 24",
|
|
82
|
+
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"/>',
|
|
83
|
+
},
|
|
84
|
+
arrowRight: {
|
|
85
|
+
name: "Arrow Right",
|
|
86
|
+
viewBox: "0 0 24 24",
|
|
87
|
+
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"/>',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns raw inline SVG markup for a standard UI icon.
|
|
93
|
+
*/
|
|
94
|
+
export function renderUiIconSvg(iconName: string, className: string = "w-5 h-5 inline-block"): string {
|
|
95
|
+
const icon = UI_ICONS[iconName.toLowerCase()] || UI_ICONS.check;
|
|
96
|
+
return `<svg class="${className}" viewBox="${icon.viewBox}" aria-hidden="true">${icon.svgPath}</svg>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Semantically resolves the most relevant UI icon based on feature text keywords.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveFeatureIcon(featureText: string): string {
|
|
103
|
+
const lower = featureText.toLowerCase();
|
|
104
|
+
|
|
105
|
+
if (lower.includes("secur") || lower.includes("rgpd") || lower.includes("gdpr") || lower.includes("protect") || lower.includes("bank")) {
|
|
106
|
+
return "shield";
|
|
107
|
+
}
|
|
108
|
+
if (lower.includes("sync") || lower.includes("speed") || lower.includes("instant") || lower.includes("real-time") || lower.includes("fast") || lower.includes("rapide")) {
|
|
109
|
+
return "zap";
|
|
110
|
+
}
|
|
111
|
+
if (lower.includes("ai") || lower.includes("ia") || lower.includes("auto") || lower.includes("smart") || lower.includes("intel")) {
|
|
112
|
+
return "sparkles";
|
|
113
|
+
}
|
|
114
|
+
if (lower.includes("chart") || lower.includes("analyt") || lower.includes("report") || lower.includes("stat") || lower.includes("roi") || lower.includes("kpi")) {
|
|
115
|
+
return "chart";
|
|
116
|
+
}
|
|
117
|
+
if (lower.includes("schedul") || lower.includes("calen") || lower.includes("plan") || lower.includes("agenda")) {
|
|
118
|
+
return "calendar";
|
|
119
|
+
}
|
|
120
|
+
if (lower.includes("user") || lower.includes("team") || lower.includes("collab") || lower.includes("client") || lower.includes("contact")) {
|
|
121
|
+
return "users";
|
|
122
|
+
}
|
|
123
|
+
if (lower.includes("price") || lower.includes("tarif") || lower.includes("cost") || lower.includes("devis") || lower.includes("factur")) {
|
|
124
|
+
return "euro";
|
|
125
|
+
}
|
|
126
|
+
if (lower.includes("local") || lower.includes("city") || lower.includes("ville") || lower.includes("map") || lower.includes("gps")) {
|
|
127
|
+
return "mapPin";
|
|
128
|
+
}
|
|
129
|
+
if (lower.includes("24/7") || lower.includes("support") || lower.includes("hour") || lower.includes("time") || lower.includes("temps")) {
|
|
130
|
+
return "clock";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return "zap";
|
|
134
|
+
}
|