@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.
- package/connectors/wordpress/lynxseo-connector.php +407 -356
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/dist/google-business-profile.d.ts +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +124 -0
- package/dist/index.mjs +124 -0
- package/dist/video-youtube-analyzer.d.ts +28 -0
- package/package.json +1 -1
- package/src/google-business-profile.ts +98 -0
- package/src/index.ts +2 -0
- package/src/video-youtube-analyzer.ts +81 -0
|
Binary file
|
|
@@ -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 →</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, """)}"
|
|
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 →</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, """)}"
|
|
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
|
@@ -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 →</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, '"')}"
|
|
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
|
+
}
|