@lynxflow/seo-engine 1.6.0 → 1.6.2

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.
@@ -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"
@@ -141,6 +142,7 @@ export interface GeneratedPageMeta {
141
142
  robots: "index, follow" | "noindex, follow";
142
143
  schemaGraph: SchemaOrgGraph;
143
144
  disclaimerText?: string;
145
+ ogImageUrl?: string;
144
146
  neighboringLinks?: { name: string; url: string }[];
145
147
  service?: PseoService;
146
148
  location?: PseoLocation;
@@ -824,6 +826,12 @@ export class PseoMatrixEngine {
824
826
  allPages.push(...this.generateCalculatorMatrix(domain, data.calculators, options));
825
827
  }
826
828
 
829
+ for (const p of allPages) {
830
+ if (!p.ogImageUrl) {
831
+ p.ogImageUrl = OgImageGenerator.generateOgImageUrl(domain, p, options.brandName);
832
+ }
833
+ }
834
+
827
835
  return allPages;
828
836
  }
829
837
 
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
+ }
@@ -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
+ }