@lynxflow/seo-engine 1.7.4 → 1.7.6

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.
@@ -0,0 +1,27 @@
1
+ export interface GoogleBusinessProfileConfig {
2
+ placeId?: string;
3
+ businessName: string;
4
+ category: string;
5
+ streetAddress: string;
6
+ addressLocality: string;
7
+ postalCode: string;
8
+ addressCountry: string;
9
+ telephone: string;
10
+ priceRange?: string;
11
+ latitude?: number;
12
+ longitude?: number;
13
+ openingHours?: string[];
14
+ ratingValue?: number;
15
+ reviewCount?: number;
16
+ googleMapsUrl?: string;
17
+ }
18
+ export declare class GoogleBusinessProfileEngine {
19
+ /**
20
+ * Generates Schema.org LocalBusiness JSON-LD structure matching Google specifications
21
+ */
22
+ static generateLocalBusinessSchema(config: GoogleBusinessProfileConfig): Record<string, any>;
23
+ /**
24
+ * Generates interactive HTML Badge with Google Review stars and direct Maps link
25
+ */
26
+ static generateGbpBadgeHtml(config: GoogleBusinessProfileConfig): string;
27
+ }
package/dist/index.d.ts CHANGED
@@ -62,6 +62,8 @@ export * from "./yoast-parity";
62
62
  export * from "./social-growth-suite";
63
63
  export * from "./team-rbac";
64
64
  export * from "./real-reviews-sync";
65
+ export * from "./google-business-profile";
66
+ export * from "./video-youtube-analyzer";
65
67
  import { LynxSeoEngine, type EngineConfig } from "./engine";
66
68
  import { ApiKeyGuardian } from "./auth-key";
67
69
  import { TokenQuotaManager } from "./token-quota-manager";
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ __export(exports_src, {
50
50
  createLynxSeoEngine: () => createLynxSeoEngine,
51
51
  cleanSeoSlug: () => cleanSeoSlug,
52
52
  YoastParityEngine: () => YoastParityEngine,
53
+ VideoYouTubeAnalyzer: () => VideoYouTubeAnalyzer,
53
54
  UrlyticsEngine: () => UrlyticsEngine,
54
55
  UI_ICONS: () => UI_ICONS,
55
56
  TokenQuotaManager: () => TokenQuotaManager,
@@ -91,6 +92,7 @@ __export(exports_src, {
91
92
  IndexNowClient: () => IndexNowClient,
92
93
  I18nDetector: () => I18nDetector,
93
94
  HyperswitchGateway: () => HyperswitchGateway,
95
+ GoogleBusinessProfileEngine: () => GoogleBusinessProfileEngine,
94
96
  GeoMeshLinkingEngine: () => GeoMeshLinkingEngine,
95
97
  FreePublicToolsEngine: () => FreePublicToolsEngine,
96
98
  ExtendedSchemaGraphBuilder: () => ExtendedSchemaGraphBuilder,
@@ -7076,6 +7078,128 @@ class RealReviewsSyncEngine {
7076
7078
  `;
7077
7079
  }
7078
7080
  }
7081
+ // src/google-business-profile.ts
7082
+ class GoogleBusinessProfileEngine {
7083
+ static generateLocalBusinessSchema(config) {
7084
+ const schema = {
7085
+ "@context": "https://schema.org",
7086
+ "@type": config.category || "LocalBusiness",
7087
+ name: config.businessName,
7088
+ address: {
7089
+ "@type": "PostalAddress",
7090
+ streetAddress: config.streetAddress,
7091
+ addressLocality: config.addressLocality,
7092
+ postalCode: config.postalCode,
7093
+ addressCountry: config.addressCountry || "FR"
7094
+ },
7095
+ telephone: config.telephone,
7096
+ priceRange: config.priceRange || "€€"
7097
+ };
7098
+ if (config.latitude && config.longitude) {
7099
+ schema.geo = {
7100
+ "@type": "GeoCoordinates",
7101
+ latitude: config.latitude,
7102
+ longitude: config.longitude
7103
+ };
7104
+ }
7105
+ if (config.openingHours && config.openingHours.length > 0) {
7106
+ schema.openingHours = config.openingHours;
7107
+ }
7108
+ if (config.ratingValue && config.reviewCount) {
7109
+ schema.aggregateRating = {
7110
+ "@type": "AggregateRating",
7111
+ ratingValue: config.ratingValue.toString(),
7112
+ reviewCount: config.reviewCount.toString(),
7113
+ bestRating: "5",
7114
+ worstRating: "1"
7115
+ };
7116
+ }
7117
+ if (config.googleMapsUrl || config.placeId) {
7118
+ schema.hasMap = config.googleMapsUrl || `https://www.google.com/maps/place/?q=place_id:${config.placeId}`;
7119
+ }
7120
+ return schema;
7121
+ }
7122
+ static generateGbpBadgeHtml(config) {
7123
+ const stars = "★".repeat(Math.round(config.ratingValue || 5)) + "☆".repeat(5 - Math.round(config.ratingValue || 5));
7124
+ const mapsUrl = config.googleMapsUrl || (config.placeId ? `https://www.google.com/maps/place/?q=place_id:${config.placeId}` : "#");
7125
+ return `
7126
+ <div style="background:#ffffff;border:1px solid #e2e8f0;border-radius:12px;padding:16px 20px;max-width:420px;box-shadow:0 2px 4px rgba(0,0,0,0.04);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
7127
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
7128
+ <div style="display:flex;align-items:center;gap:8px;">
7129
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#4285F4"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
7130
+ <strong style="font-size:15px;color:#0f172a;">${config.businessName}</strong>
7131
+ </div>
7132
+ <span style="font-size:11px;font-weight:700;color:#166534;background:#dcfce7;padding:2px 8px;border-radius:999px;">Vérifié Google</span>
7133
+ </div>
7134
+ <div style="font-size:13px;color:#475569;margin-bottom:8px;">
7135
+ ${config.streetAddress}, ${config.postalCode} ${config.addressLocality}
7136
+ </div>
7137
+ <div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:10px;">
7138
+ <div style="display:flex;align-items:center;gap:6px;">
7139
+ <span style="color:#f59e0b;letter-spacing:1px;font-size:14px;">${stars}</span>
7140
+ <strong style="font-size:13px;color:#0f172a;">${config.ratingValue || "4.9"}/5</strong>
7141
+ <span style="font-size:12px;color:#64748b;">(${config.reviewCount || "128"} avis)</span>
7142
+ </div>
7143
+ <a href="${mapsUrl}" target="_blank" rel="noopener noreferrer" style="font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;">Voir sur Google Maps &rarr;</a>
7144
+ </div>
7145
+ </div>
7146
+ `.trim();
7147
+ }
7148
+ }
7149
+ // src/video-youtube-analyzer.ts
7150
+ class VideoYouTubeAnalyzer {
7151
+ static extractVideoId(urlOrIframe) {
7152
+ const regExp = /(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
7153
+ const match = urlOrIframe.match(regExp);
7154
+ return match ? match[1] : null;
7155
+ }
7156
+ static generateVideoObjectSchema(meta) {
7157
+ const videoId = meta.videoId;
7158
+ const thumbnail = meta.thumbnailUrl || `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
7159
+ const embedUrl = meta.embedUrl || `https://www.youtube.com/embed/${videoId}`;
7160
+ const schema = {
7161
+ "@context": "https://schema.org",
7162
+ "@type": "VideoObject",
7163
+ name: meta.title,
7164
+ description: meta.description,
7165
+ thumbnailUrl: [thumbnail],
7166
+ uploadDate: meta.uploadDate || new Date().toISOString().split("T")[0],
7167
+ embedUrl,
7168
+ contentUrl: `https://www.youtube.com/watch?v=${videoId}`
7169
+ };
7170
+ if (meta.duration) {
7171
+ schema.duration = meta.duration;
7172
+ }
7173
+ if (meta.chapters && meta.chapters.length > 0) {
7174
+ schema.hasPart = meta.chapters.map((c) => ({
7175
+ "@type": "Clip",
7176
+ name: c.title,
7177
+ startOffset: c.time,
7178
+ url: `https://www.youtube.com/watch?v=${videoId}&t=${c.time}s`
7179
+ }));
7180
+ }
7181
+ return schema;
7182
+ }
7183
+ static generateSeoVideoEmbedHtml(meta) {
7184
+ const videoId = meta.videoId;
7185
+ const jsonLd = JSON.stringify(this.generateVideoObjectSchema(meta));
7186
+ return `
7187
+ <div class="lynxseo-video-wrapper" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);margin:24px 0;">
7188
+ <iframe
7189
+ src="https://www.youtube.com/embed/${videoId}"
7190
+ title="${meta.title.replace(/"/g, "&quot;")}"
7191
+ style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
7192
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
7193
+ allowfullscreen
7194
+ loading="lazy">
7195
+ </iframe>
7196
+ </div>
7197
+ <script type="application/ld+json">
7198
+ ${jsonLd}
7199
+ </script>
7200
+ `.trim();
7201
+ }
7202
+ }
7079
7203
  // src/index.ts
7080
7204
  function createLynxSeoEngine(config) {
7081
7205
  return new LynxSeoEngine(config);
package/dist/index.mjs CHANGED
@@ -6964,6 +6964,128 @@ class RealReviewsSyncEngine {
6964
6964
  `;
6965
6965
  }
6966
6966
  }
6967
+ // src/google-business-profile.ts
6968
+ class GoogleBusinessProfileEngine {
6969
+ static generateLocalBusinessSchema(config) {
6970
+ const schema = {
6971
+ "@context": "https://schema.org",
6972
+ "@type": config.category || "LocalBusiness",
6973
+ name: config.businessName,
6974
+ address: {
6975
+ "@type": "PostalAddress",
6976
+ streetAddress: config.streetAddress,
6977
+ addressLocality: config.addressLocality,
6978
+ postalCode: config.postalCode,
6979
+ addressCountry: config.addressCountry || "FR"
6980
+ },
6981
+ telephone: config.telephone,
6982
+ priceRange: config.priceRange || "€€"
6983
+ };
6984
+ if (config.latitude && config.longitude) {
6985
+ schema.geo = {
6986
+ "@type": "GeoCoordinates",
6987
+ latitude: config.latitude,
6988
+ longitude: config.longitude
6989
+ };
6990
+ }
6991
+ if (config.openingHours && config.openingHours.length > 0) {
6992
+ schema.openingHours = config.openingHours;
6993
+ }
6994
+ if (config.ratingValue && config.reviewCount) {
6995
+ schema.aggregateRating = {
6996
+ "@type": "AggregateRating",
6997
+ ratingValue: config.ratingValue.toString(),
6998
+ reviewCount: config.reviewCount.toString(),
6999
+ bestRating: "5",
7000
+ worstRating: "1"
7001
+ };
7002
+ }
7003
+ if (config.googleMapsUrl || config.placeId) {
7004
+ schema.hasMap = config.googleMapsUrl || `https://www.google.com/maps/place/?q=place_id:${config.placeId}`;
7005
+ }
7006
+ return schema;
7007
+ }
7008
+ static generateGbpBadgeHtml(config) {
7009
+ const stars = "★".repeat(Math.round(config.ratingValue || 5)) + "☆".repeat(5 - Math.round(config.ratingValue || 5));
7010
+ const mapsUrl = config.googleMapsUrl || (config.placeId ? `https://www.google.com/maps/place/?q=place_id:${config.placeId}` : "#");
7011
+ return `
7012
+ <div style="background:#ffffff;border:1px solid #e2e8f0;border-radius:12px;padding:16px 20px;max-width:420px;box-shadow:0 2px 4px rgba(0,0,0,0.04);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
7013
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
7014
+ <div style="display:flex;align-items:center;gap:8px;">
7015
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#4285F4"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
7016
+ <strong style="font-size:15px;color:#0f172a;">${config.businessName}</strong>
7017
+ </div>
7018
+ <span style="font-size:11px;font-weight:700;color:#166534;background:#dcfce7;padding:2px 8px;border-radius:999px;">Vérifié Google</span>
7019
+ </div>
7020
+ <div style="font-size:13px;color:#475569;margin-bottom:8px;">
7021
+ ${config.streetAddress}, ${config.postalCode} ${config.addressLocality}
7022
+ </div>
7023
+ <div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:10px;">
7024
+ <div style="display:flex;align-items:center;gap:6px;">
7025
+ <span style="color:#f59e0b;letter-spacing:1px;font-size:14px;">${stars}</span>
7026
+ <strong style="font-size:13px;color:#0f172a;">${config.ratingValue || "4.9"}/5</strong>
7027
+ <span style="font-size:12px;color:#64748b;">(${config.reviewCount || "128"} avis)</span>
7028
+ </div>
7029
+ <a href="${mapsUrl}" target="_blank" rel="noopener noreferrer" style="font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;">Voir sur Google Maps &rarr;</a>
7030
+ </div>
7031
+ </div>
7032
+ `.trim();
7033
+ }
7034
+ }
7035
+ // src/video-youtube-analyzer.ts
7036
+ class VideoYouTubeAnalyzer {
7037
+ static extractVideoId(urlOrIframe) {
7038
+ const regExp = /(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
7039
+ const match = urlOrIframe.match(regExp);
7040
+ return match ? match[1] : null;
7041
+ }
7042
+ static generateVideoObjectSchema(meta) {
7043
+ const videoId = meta.videoId;
7044
+ const thumbnail = meta.thumbnailUrl || `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
7045
+ const embedUrl = meta.embedUrl || `https://www.youtube.com/embed/${videoId}`;
7046
+ const schema = {
7047
+ "@context": "https://schema.org",
7048
+ "@type": "VideoObject",
7049
+ name: meta.title,
7050
+ description: meta.description,
7051
+ thumbnailUrl: [thumbnail],
7052
+ uploadDate: meta.uploadDate || new Date().toISOString().split("T")[0],
7053
+ embedUrl,
7054
+ contentUrl: `https://www.youtube.com/watch?v=${videoId}`
7055
+ };
7056
+ if (meta.duration) {
7057
+ schema.duration = meta.duration;
7058
+ }
7059
+ if (meta.chapters && meta.chapters.length > 0) {
7060
+ schema.hasPart = meta.chapters.map((c) => ({
7061
+ "@type": "Clip",
7062
+ name: c.title,
7063
+ startOffset: c.time,
7064
+ url: `https://www.youtube.com/watch?v=${videoId}&t=${c.time}s`
7065
+ }));
7066
+ }
7067
+ return schema;
7068
+ }
7069
+ static generateSeoVideoEmbedHtml(meta) {
7070
+ const videoId = meta.videoId;
7071
+ const jsonLd = JSON.stringify(this.generateVideoObjectSchema(meta));
7072
+ return `
7073
+ <div class="lynxseo-video-wrapper" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);margin:24px 0;">
7074
+ <iframe
7075
+ src="https://www.youtube.com/embed/${videoId}"
7076
+ title="${meta.title.replace(/"/g, "&quot;")}"
7077
+ style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
7078
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
7079
+ allowfullscreen
7080
+ loading="lazy">
7081
+ </iframe>
7082
+ </div>
7083
+ <script type="application/ld+json">
7084
+ ${jsonLd}
7085
+ </script>
7086
+ `.trim();
7087
+ }
7088
+ }
6967
7089
  // src/index.ts
6968
7090
  function createLynxSeoEngine(config) {
6969
7091
  return new LynxSeoEngine(config);
@@ -7021,6 +7143,7 @@ export {
7021
7143
  createLynxSeoEngine,
7022
7144
  cleanSeoSlug,
7023
7145
  YoastParityEngine,
7146
+ VideoYouTubeAnalyzer,
7024
7147
  UrlyticsEngine,
7025
7148
  UI_ICONS,
7026
7149
  TokenQuotaManager,
@@ -7062,6 +7185,7 @@ export {
7062
7185
  IndexNowClient,
7063
7186
  I18nDetector,
7064
7187
  HyperswitchGateway,
7188
+ GoogleBusinessProfileEngine,
7065
7189
  GeoMeshLinkingEngine,
7066
7190
  FreePublicToolsEngine,
7067
7191
  ExtendedSchemaGraphBuilder,
@@ -0,0 +1,28 @@
1
+ export interface YouTubeVideoMetadata {
2
+ videoId: string;
3
+ title: string;
4
+ description: string;
5
+ uploadDate?: string;
6
+ duration?: string;
7
+ thumbnailUrl?: string;
8
+ embedUrl?: string;
9
+ hasChapters?: boolean;
10
+ chapters?: Array<{
11
+ time: number;
12
+ title: string;
13
+ }>;
14
+ }
15
+ export declare class VideoYouTubeAnalyzer {
16
+ /**
17
+ * Extracts YouTube Video ID from any standard URL or iframe string
18
+ */
19
+ static extractVideoId(urlOrIframe: string): string | null;
20
+ /**
21
+ * Generates Schema.org VideoObject JSON-LD for rich snippets on Google Video Carousel
22
+ */
23
+ static generateVideoObjectSchema(meta: YouTubeVideoMetadata): Record<string, any>;
24
+ /**
25
+ * Generates responsive, lazy-loaded, SEO-optimized HTML embed with Schema markup
26
+ */
27
+ static generateSeoVideoEmbedHtml(meta: YouTubeVideoMetadata): string;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -0,0 +1,98 @@
1
+ export interface GoogleBusinessProfileConfig {
2
+ placeId?: string;
3
+ businessName: string;
4
+ category: string;
5
+ streetAddress: string;
6
+ addressLocality: string;
7
+ postalCode: string;
8
+ addressCountry: string;
9
+ telephone: string;
10
+ priceRange?: string;
11
+ latitude?: number;
12
+ longitude?: number;
13
+ openingHours?: string[]; // e.g. ["Mo-Fr 09:00-18:00", "Sa 10:00-16:00"]
14
+ ratingValue?: number;
15
+ reviewCount?: number;
16
+ googleMapsUrl?: string;
17
+ }
18
+
19
+ export class GoogleBusinessProfileEngine {
20
+ /**
21
+ * Generates Schema.org LocalBusiness JSON-LD structure matching Google specifications
22
+ */
23
+ static generateLocalBusinessSchema(config: GoogleBusinessProfileConfig) {
24
+ const schema: Record<string, any> = {
25
+ "@context": "https://schema.org",
26
+ "@type": config.category || "LocalBusiness",
27
+ "name": config.businessName,
28
+ "address": {
29
+ "@type": "PostalAddress",
30
+ "streetAddress": config.streetAddress,
31
+ "addressLocality": config.addressLocality,
32
+ "postalCode": config.postalCode,
33
+ "addressCountry": config.addressCountry || "FR"
34
+ },
35
+ "telephone": config.telephone,
36
+ "priceRange": config.priceRange || "€€"
37
+ };
38
+
39
+ if (config.latitude && config.longitude) {
40
+ schema.geo = {
41
+ "@type": "GeoCoordinates",
42
+ "latitude": config.latitude,
43
+ "longitude": config.longitude
44
+ };
45
+ }
46
+
47
+ if (config.openingHours && config.openingHours.length > 0) {
48
+ schema.openingHours = config.openingHours;
49
+ }
50
+
51
+ if (config.ratingValue && config.reviewCount) {
52
+ schema.aggregateRating = {
53
+ "@type": "AggregateRating",
54
+ "ratingValue": config.ratingValue.toString(),
55
+ "reviewCount": config.reviewCount.toString(),
56
+ "bestRating": "5",
57
+ "worstRating": "1"
58
+ };
59
+ }
60
+
61
+ if (config.googleMapsUrl || config.placeId) {
62
+ schema.hasMap = config.googleMapsUrl || `https://www.google.com/maps/place/?q=place_id:${config.placeId}`;
63
+ }
64
+
65
+ return schema;
66
+ }
67
+
68
+ /**
69
+ * Generates interactive HTML Badge with Google Review stars and direct Maps link
70
+ */
71
+ static generateGbpBadgeHtml(config: GoogleBusinessProfileConfig): string {
72
+ const stars = "★".repeat(Math.round(config.ratingValue || 5)) + "☆".repeat(5 - Math.round(config.ratingValue || 5));
73
+ const mapsUrl = config.googleMapsUrl || (config.placeId ? `https://www.google.com/maps/place/?q=place_id:${config.placeId}` : "#");
74
+
75
+ return `
76
+ <div style="background:#ffffff;border:1px solid #e2e8f0;border-radius:12px;padding:16px 20px;max-width:420px;box-shadow:0 2px 4px rgba(0,0,0,0.04);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
77
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
78
+ <div style="display:flex;align-items:center;gap:8px;">
79
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#4285F4"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
80
+ <strong style="font-size:15px;color:#0f172a;">${config.businessName}</strong>
81
+ </div>
82
+ <span style="font-size:11px;font-weight:700;color:#166534;background:#dcfce7;padding:2px 8px;border-radius:999px;">Vérifié Google</span>
83
+ </div>
84
+ <div style="font-size:13px;color:#475569;margin-bottom:8px;">
85
+ ${config.streetAddress}, ${config.postalCode} ${config.addressLocality}
86
+ </div>
87
+ <div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f1f5f9;padding-top:10px;">
88
+ <div style="display:flex;align-items:center;gap:6px;">
89
+ <span style="color:#f59e0b;letter-spacing:1px;font-size:14px;">${stars}</span>
90
+ <strong style="font-size:13px;color:#0f172a;">${config.ratingValue || "4.9"}/5</strong>
91
+ <span style="font-size:12px;color:#64748b;">(${config.reviewCount || "128"} avis)</span>
92
+ </div>
93
+ <a href="${mapsUrl}" target="_blank" rel="noopener noreferrer" style="font-size:12px;color:#2563eb;font-weight:600;text-decoration:none;">Voir sur Google Maps &rarr;</a>
94
+ </div>
95
+ </div>
96
+ `.trim();
97
+ }
98
+ }
package/src/index.ts CHANGED
@@ -63,6 +63,8 @@ export * from "./yoast-parity";
63
63
  export * from "./social-growth-suite";
64
64
  export * from "./team-rbac";
65
65
  export * from "./real-reviews-sync";
66
+ export * from "./google-business-profile";
67
+ export * from "./video-youtube-analyzer";
66
68
 
67
69
  import { LynxSeoEngine, type EngineConfig } from "./engine";
68
70
  import { ApiKeyGuardian } from "./auth-key";
@@ -0,0 +1,81 @@
1
+ export interface YouTubeVideoMetadata {
2
+ videoId: string;
3
+ title: string;
4
+ description: string;
5
+ uploadDate?: string;
6
+ duration?: string; // ISO 8601 e.g. "PT5M30S"
7
+ thumbnailUrl?: string;
8
+ embedUrl?: string;
9
+ hasChapters?: boolean;
10
+ chapters?: Array<{ time: number; title: string }>;
11
+ }
12
+
13
+ export class VideoYouTubeAnalyzer {
14
+ /**
15
+ * Extracts YouTube Video ID from any standard URL or iframe string
16
+ */
17
+ static extractVideoId(urlOrIframe: string): string | null {
18
+ const regExp = /(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
19
+ const match = urlOrIframe.match(regExp);
20
+ return match ? match[1] : null;
21
+ }
22
+
23
+ /**
24
+ * Generates Schema.org VideoObject JSON-LD for rich snippets on Google Video Carousel
25
+ */
26
+ static generateVideoObjectSchema(meta: YouTubeVideoMetadata) {
27
+ const videoId = meta.videoId;
28
+ const thumbnail = meta.thumbnailUrl || `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
29
+ const embedUrl = meta.embedUrl || `https://www.youtube.com/embed/${videoId}`;
30
+
31
+ const schema: Record<string, any> = {
32
+ "@context": "https://schema.org",
33
+ "@type": "VideoObject",
34
+ "name": meta.title,
35
+ "description": meta.description,
36
+ "thumbnailUrl": [thumbnail],
37
+ "uploadDate": meta.uploadDate || new Date().toISOString().split("T")[0],
38
+ "embedUrl": embedUrl,
39
+ "contentUrl": `https://www.youtube.com/watch?v=${videoId}`
40
+ };
41
+
42
+ if (meta.duration) {
43
+ schema.duration = meta.duration;
44
+ }
45
+
46
+ if (meta.chapters && meta.chapters.length > 0) {
47
+ schema.hasPart = meta.chapters.map(c => ({
48
+ "@type": "Clip",
49
+ "name": c.title,
50
+ "startOffset": c.time,
51
+ "url": `https://www.youtube.com/watch?v=${videoId}&t=${c.time}s`
52
+ }));
53
+ }
54
+
55
+ return schema;
56
+ }
57
+
58
+ /**
59
+ * Generates responsive, lazy-loaded, SEO-optimized HTML embed with Schema markup
60
+ */
61
+ static generateSeoVideoEmbedHtml(meta: YouTubeVideoMetadata): string {
62
+ const videoId = meta.videoId;
63
+ const jsonLd = JSON.stringify(this.generateVideoObjectSchema(meta));
64
+
65
+ return `
66
+ <div class="lynxseo-video-wrapper" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);margin:24px 0;">
67
+ <iframe
68
+ src="https://www.youtube.com/embed/${videoId}"
69
+ title="${meta.title.replace(/"/g, '&quot;')}"
70
+ style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
71
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
72
+ allowfullscreen
73
+ loading="lazy">
74
+ </iframe>
75
+ </div>
76
+ <script type="application/ld+json">
77
+ ${jsonLd}
78
+ </script>
79
+ `.trim();
80
+ }
81
+ }