@lynxflow/seo-engine 1.8.30 → 2.0.1
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/README.md +60 -117
- package/dist/ai-copilot-client.d.ts +2 -0
- package/dist/built-in-locations.d.ts +22 -0
- package/dist/content-ai-arsenal.d.ts +28 -0
- package/dist/existing-media-harvester.d.ts +33 -0
- package/dist/extended-schemas.d.ts +84 -0
- package/dist/features-knowledge-harvester.d.ts +56 -0
- package/dist/geo-citation-scorer.d.ts +34 -0
- package/dist/i18n-detector.d.ts +1 -1
- package/dist/index.d.ts +32 -7
- package/dist/index.js +567 -83
- package/dist/index.mjs +2062 -169
- package/dist/internal-pagerank-graph.d.ts +59 -0
- package/dist/knowledge-bank-builder.d.ts +91 -0
- package/dist/marketing-skills-engine.d.ts +74 -0
- package/dist/matrix-engine.d.ts +29 -0
- package/dist/power-words-psychology.d.ts +27 -15
- package/dist/public-manifest-engine.d.ts +58 -0
- package/dist/rag-knowledge-engine.d.ts +50 -0
- package/dist/rate-limited-translator.d.ts +19 -0
- package/dist/token-quota-manager.d.ts +4 -0
- package/package.json +11 -3
- package/TUTORIEL_INTEGRATION_SITE.md +0 -283
- package/connectors/cloudflare-worker/worker.js +0 -106
- package/connectors/laravel/LynxSeoController.php +0 -37
- package/connectors/wordpress/lynxseo-admin-app.js +0 -2611
- package/connectors/wordpress/lynxseo-connector.php +0 -1712
- package/connectors/wordpress/lynxseo-connector.zip +0 -0
- package/lynxflow-seo-engine-1.2.0.tgz +0 -0
- package/src/ad-intelligence-cro.ts +0 -140
- package/src/ai-bots-log-analyzer.ts +0 -127
- package/src/ai-copilot-client.ts +0 -99
- package/src/analytics-client.ts +0 -315
- package/src/auth-key.ts +0 -262
- package/src/backlinks-client.ts +0 -85
- package/src/brand-dna-calendar.ts +0 -120
- package/src/brand-icons.ts +0 -138
- package/src/built-in-locations.ts +0 -170
- package/src/content-ai-arsenal.ts +0 -268
- package/src/copy-frameworks-master.ts +0 -89
- package/src/cro-copywriting-engine.ts +0 -105
- package/src/crosslink-scorer.ts +0 -101
- package/src/embeddable-seo-widget.ts +0 -57
- package/src/engine.test.ts +0 -386
- package/src/engine.ts +0 -813
- package/src/existing-media-harvester.ts +0 -98
- package/src/extended-schemas.ts +0 -589
- package/src/features-knowledge-harvester.ts +0 -190
- package/src/free-public-tools.ts +0 -224
- package/src/geo-mesh-linking.ts +0 -93
- package/src/google-business-profile.ts +0 -280
- package/src/i18n-detector.ts +0 -98
- package/src/i18n-dictionary.ts +0 -422
- package/src/index.ts +0 -164
- package/src/indexnow-client.ts +0 -94
- package/src/instant-matrix-search.ts +0 -48
- package/src/isr-cache-manager.ts +0 -78
- package/src/keyword-permutator.ts +0 -118
- package/src/knowledge-bank-builder.ts +0 -299
- package/src/knowledge-graph-linker.ts +0 -81
- package/src/lago-token-meter.ts +0 -120
- package/src/legal-disclaimers.ts +0 -407
- package/src/llm-content-cleaner.ts +0 -64
- package/src/llm-prompt.ts +0 -323
- package/src/master-marketing-engine.ts +0 -314
- package/src/matrix-engine.ts +0 -1204
- package/src/mcp-seo-server.ts +0 -108
- package/src/ngram-density-analyzer.ts +0 -108
- package/src/og-image-generator.ts +0 -109
- package/src/power-words-psychology.ts +0 -369
- package/src/public-manifest-engine.ts +0 -109
- package/src/rag-knowledge-engine.ts +0 -163
- package/src/rank-math-parity.ts +0 -235
- package/src/rate-limited-translator.ts +0 -110
- package/src/real-reviews-sync.ts +0 -207
- package/src/rss-syndication-feed.ts +0 -56
- package/src/schema-builder.ts +0 -134
- package/src/seo-opportunities-decay.ts +0 -154
- package/src/serp-client.ts +0 -89
- package/src/serp-history-alerts.ts +0 -75
- package/src/site-auditor.ts +0 -83
- package/src/site-crawler.ts +0 -406
- package/src/slug-engine.ts +0 -183
- package/src/social-ads-seo.ts +0 -211
- package/src/social-growth-suite.ts +0 -310
- package/src/social-trend-seo.ts +0 -103
- package/src/social-video-seo.ts +0 -100
- package/src/team-rbac.ts +0 -61
- package/src/technical-rules-auditor.ts +0 -171
- package/src/token-quota-manager.ts +0 -91
- package/src/types.ts +0 -143
- package/src/ui-icons.ts +0 -134
- package/src/urlytics-engine.ts +0 -97
- package/src/video-youtube-analyzer.ts +0 -81
- package/src/yoast-parity.ts +0 -161
- package/tsconfig.json +0 -15
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📸 Existing Media Harvester & Asset Pool Engine
|
|
3
|
-
*
|
|
4
|
-
* Reuses real, pre-existing photos from the client's website & WordPress Media Library:
|
|
5
|
-
* 1. 0 € GPU Cost & 0s Compute (Zero VRAM Saturation Guarantee)
|
|
6
|
-
* 2. Maximum Google E-E-A-T Authenticity (Real brand photos > Generic AI images)
|
|
7
|
-
* 3. Rotates existing photos across the 6 page variations
|
|
8
|
-
* 4. Dynamically injects localized <img alt="..."> tags across 40+ languages in RAM (< 0.01ms)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface HarvestedMediaAsset {
|
|
12
|
-
id?: string | number;
|
|
13
|
-
url: string;
|
|
14
|
-
originalAlt?: string;
|
|
15
|
-
matchedKeywords?: string[];
|
|
16
|
-
width?: number;
|
|
17
|
-
height?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class ExistingMediaHarvester {
|
|
21
|
-
/**
|
|
22
|
-
* Extracts all existing image URLs from HTML pages or WordPress Media API
|
|
23
|
-
*/
|
|
24
|
-
static harvestFromHtml(htmlContent: string, baseUrl = ""): HarvestedMediaAsset[] {
|
|
25
|
-
const assets: HarvestedMediaAsset[] = [];
|
|
26
|
-
const imgRegex = /<img\s+[^>]*src=["']([^"']+)["'][^>]*>/gi;
|
|
27
|
-
const altRegex = /alt=["']([^"']*)["']/i;
|
|
28
|
-
|
|
29
|
-
let match: RegExpExecArray | null;
|
|
30
|
-
const seenUrls = new Set<string>();
|
|
31
|
-
|
|
32
|
-
while ((match = imgRegex.exec(htmlContent)) !== null) {
|
|
33
|
-
const fullImgTag = match[0];
|
|
34
|
-
let src = match[1];
|
|
35
|
-
|
|
36
|
-
// Exclude tiny tracking pixels, icons or avatars
|
|
37
|
-
if (
|
|
38
|
-
src.includes("gravatar.com") ||
|
|
39
|
-
src.includes("icon-") ||
|
|
40
|
-
src.includes(".svg") ||
|
|
41
|
-
src.includes("1x1")
|
|
42
|
-
) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (src.startsWith("/") && baseUrl) {
|
|
47
|
-
src = `${baseUrl.replace(/\/$/, "")}${src}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!seenUrls.has(src)) {
|
|
51
|
-
seenUrls.add(src);
|
|
52
|
-
const altMatch = altRegex.exec(fullImgTag);
|
|
53
|
-
const originalAlt = altMatch ? altMatch[1] : "";
|
|
54
|
-
|
|
55
|
-
assets.push({
|
|
56
|
-
url: src,
|
|
57
|
-
originalAlt,
|
|
58
|
-
matchedKeywords: this.extractKeywordsFromUrl(src, originalAlt),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return assets;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Matches the best existing photo from the pool for a given service & variation
|
|
68
|
-
*/
|
|
69
|
-
static selectBestImageForVariant(params: {
|
|
70
|
-
serviceName: string;
|
|
71
|
-
variantIndex: number;
|
|
72
|
-
mediaPool: HarvestedMediaAsset[];
|
|
73
|
-
fallbackPlaceholderUrl?: string;
|
|
74
|
-
}): string {
|
|
75
|
-
if (!params.mediaPool || params.mediaPool.length === 0) {
|
|
76
|
-
return params.fallbackPlaceholderUrl || "https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?w=1200&q=80";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const serviceLower = params.serviceName.toLowerCase();
|
|
80
|
-
|
|
81
|
-
// 1. Try to find a photo whose filename or original alt matches the service
|
|
82
|
-
const matchingPhotos = params.mediaPool.filter((asset) =>
|
|
83
|
-
asset.matchedKeywords?.some((kw) => serviceLower.includes(kw))
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const candidatePool = matchingPhotos.length > 0 ? matchingPhotos : params.mediaPool;
|
|
87
|
-
|
|
88
|
-
// 2. Rotate deterministically across variations so each variant gets a different photo
|
|
89
|
-
const selectedIndex = params.variantIndex % candidatePool.length;
|
|
90
|
-
return candidatePool[selectedIndex].url;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private static extractKeywordsFromUrl(url: string, alt: string): string[] {
|
|
94
|
-
const filename = url.split("/").pop()?.split("?")[0] || "";
|
|
95
|
-
const cleanText = `${filename} ${alt}`.toLowerCase().replace(/[-_.]/g, " ");
|
|
96
|
-
return cleanText.split(/\s+/).filter((w) => w.length > 3);
|
|
97
|
-
}
|
|
98
|
-
}
|
package/src/extended-schemas.ts
DELETED
|
@@ -1,589 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extended Universal Schema.org Rich Graph Builder
|
|
3
|
-
* Supports 50+ Schema.org official types for Google Search Rich Results:
|
|
4
|
-
* - SoftwareApplication, WebApplication, MobileApplication
|
|
5
|
-
* - 35+ LocalBusiness Subtypes (Plumber, HVACBusiness, Electrician, Dentist, LegalService, AccountingService, etc.)
|
|
6
|
-
* - Organization with Entity SEO sameAs links (Wikidata, Crunchbase, LinkedIn)
|
|
7
|
-
* - Product, Offer, MerchantReturnPolicy
|
|
8
|
-
* - Article, TechArticle, NewsArticle
|
|
9
|
-
* - HowTo, HowToStep
|
|
10
|
-
* - FAQPage
|
|
11
|
-
* - BreadcrumbList
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export const SCHEMA_LOCAL_BUSINESS_MAP: Record<string, string> = {
|
|
15
|
-
// Trades & Construction
|
|
16
|
-
plumber: "Plumber",
|
|
17
|
-
plumbing: "Plumber",
|
|
18
|
-
hvac: "HVACBusiness",
|
|
19
|
-
electrician: "Electrician",
|
|
20
|
-
locksmith: "Locksmith",
|
|
21
|
-
roofing: "RoofingContractor",
|
|
22
|
-
painting: "HousePainter",
|
|
23
|
-
painter: "HousePainter",
|
|
24
|
-
carpenter: "HomeAndConstructionBusiness",
|
|
25
|
-
masonry: "GeneralContractor",
|
|
26
|
-
contractor: "GeneralContractor",
|
|
27
|
-
landscaping: "LandscapeContractor",
|
|
28
|
-
gardener: "LandscapeContractor",
|
|
29
|
-
pestcontrol: "PestControlService",
|
|
30
|
-
cleaning: "CleaningService",
|
|
31
|
-
towing: "TowingService",
|
|
32
|
-
|
|
33
|
-
// Healthcare & Wellness
|
|
34
|
-
dentist: "Dentist",
|
|
35
|
-
doctor: "MedicalBusiness",
|
|
36
|
-
physician: "MedicalBusiness",
|
|
37
|
-
medical: "MedicalBusiness",
|
|
38
|
-
psychologist: "MedicalBusiness",
|
|
39
|
-
osteopath: "MedicalBusiness",
|
|
40
|
-
physiotherapist: "MedicalBusiness",
|
|
41
|
-
veterinarian: "VeterinaryCare",
|
|
42
|
-
pharmacy: "Pharmacy",
|
|
43
|
-
dayspa: "DaySpa",
|
|
44
|
-
spa: "DaySpa",
|
|
45
|
-
hairsalon: "HairSalon",
|
|
46
|
-
barber: "BarberShop",
|
|
47
|
-
beautysalon: "BeautySalon",
|
|
48
|
-
healthclub: "HealthClub",
|
|
49
|
-
gym: "HealthClub",
|
|
50
|
-
|
|
51
|
-
// Professional Services
|
|
52
|
-
lawyer: "LegalService",
|
|
53
|
-
attorney: "LegalService",
|
|
54
|
-
legalservice: "LegalService",
|
|
55
|
-
accountant: "AccountingService",
|
|
56
|
-
accounting: "AccountingService",
|
|
57
|
-
insurance: "InsuranceAgency",
|
|
58
|
-
realestate: "RealEstateAgent",
|
|
59
|
-
realtor: "RealEstateAgent",
|
|
60
|
-
notary: "Notary",
|
|
61
|
-
moving: "MovingCompany",
|
|
62
|
-
education: "EducationalOrganization",
|
|
63
|
-
school: "EducationalOrganization",
|
|
64
|
-
|
|
65
|
-
// Automotive & Hospitality
|
|
66
|
-
autorepair: "AutoRepair",
|
|
67
|
-
mechanic: "AutoRepair",
|
|
68
|
-
carrental: "AutoRental",
|
|
69
|
-
restaurant: "Restaurant",
|
|
70
|
-
hotel: "Hotel",
|
|
71
|
-
store: "Store",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export interface SchemaOrgOrganizationOpts {
|
|
75
|
-
name: string;
|
|
76
|
-
url: string;
|
|
77
|
-
logo?: string;
|
|
78
|
-
sameAs?: string[];
|
|
79
|
-
contactPoint?: {
|
|
80
|
-
telephone: string;
|
|
81
|
-
contactType: string;
|
|
82
|
-
availableLanguage: string[];
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface SchemaOrgLocalBusinessOpts {
|
|
87
|
-
name: string;
|
|
88
|
-
url: string;
|
|
89
|
-
schemaType?: string;
|
|
90
|
-
telephone?: string;
|
|
91
|
-
email?: string;
|
|
92
|
-
streetAddress?: string;
|
|
93
|
-
city: string;
|
|
94
|
-
region?: string;
|
|
95
|
-
postalCode?: string;
|
|
96
|
-
country: string;
|
|
97
|
-
latitude?: number;
|
|
98
|
-
longitude?: number;
|
|
99
|
-
openingHours?: string[];
|
|
100
|
-
priceRange?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface SchemaOrgProductOpts {
|
|
104
|
-
name: string;
|
|
105
|
-
description: string;
|
|
106
|
-
url: string;
|
|
107
|
-
brandName: string;
|
|
108
|
-
price: number;
|
|
109
|
-
currency: string;
|
|
110
|
-
image?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SchemaOrgArticleOpts {
|
|
114
|
-
headline: string;
|
|
115
|
-
description: string;
|
|
116
|
-
url: string;
|
|
117
|
-
datePublished?: string;
|
|
118
|
-
dateModified?: string;
|
|
119
|
-
authorName: string;
|
|
120
|
-
publisherName: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export class ExtendedSchemaGraphBuilder {
|
|
124
|
-
/**
|
|
125
|
-
* Resolves the most accurate Schema.org type for a business sector or keyword.
|
|
126
|
-
*/
|
|
127
|
-
static resolveBusinessType(industryOrSectorSlug: string): string {
|
|
128
|
-
const clean = industryOrSectorSlug.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
129
|
-
return SCHEMA_LOCAL_BUSINESS_MAP[clean] || "LocalBusiness";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 1. Organization with Entity SEO sameAs links
|
|
134
|
-
*/
|
|
135
|
-
static buildOrganization(opts: SchemaOrgOrganizationOpts): Record<string, unknown> {
|
|
136
|
-
return {
|
|
137
|
-
"@context": "https://schema.org",
|
|
138
|
-
"@type": "Organization",
|
|
139
|
-
name: opts.name,
|
|
140
|
-
url: opts.url,
|
|
141
|
-
logo: opts.logo,
|
|
142
|
-
sameAs: opts.sameAs || [],
|
|
143
|
-
contactPoint: opts.contactPoint
|
|
144
|
-
? {
|
|
145
|
-
"@type": "ContactPoint",
|
|
146
|
-
telephone: opts.contactPoint.telephone,
|
|
147
|
-
contactType: opts.contactPoint.contactType,
|
|
148
|
-
availableLanguage: opts.contactPoint.availableLanguage,
|
|
149
|
-
}
|
|
150
|
-
: undefined,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 2. LocalBusiness with GeoCoordinates & Address
|
|
156
|
-
*/
|
|
157
|
-
static buildLocalBusiness(opts: SchemaOrgLocalBusinessOpts): Record<string, unknown> {
|
|
158
|
-
const businessType = opts.schemaType || this.resolveBusinessType(opts.name);
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
"@context": "https://schema.org",
|
|
162
|
-
"@type": businessType,
|
|
163
|
-
name: opts.name,
|
|
164
|
-
url: opts.url,
|
|
165
|
-
telephone: opts.telephone,
|
|
166
|
-
email: opts.email,
|
|
167
|
-
priceRange: opts.priceRange || "$$",
|
|
168
|
-
address: {
|
|
169
|
-
"@type": "PostalAddress",
|
|
170
|
-
streetAddress: opts.streetAddress,
|
|
171
|
-
addressLocality: opts.city,
|
|
172
|
-
addressRegion: opts.region,
|
|
173
|
-
postalCode: opts.postalCode,
|
|
174
|
-
addressCountry: opts.country,
|
|
175
|
-
},
|
|
176
|
-
geo:
|
|
177
|
-
opts.latitude && opts.longitude
|
|
178
|
-
? {
|
|
179
|
-
"@type": "GeoCoordinates",
|
|
180
|
-
latitude: opts.latitude,
|
|
181
|
-
longitude: opts.longitude,
|
|
182
|
-
}
|
|
183
|
-
: undefined,
|
|
184
|
-
openingHoursSpecification: opts.openingHours
|
|
185
|
-
? opts.openingHours.map((hours) => ({
|
|
186
|
-
"@type": "OpeningHoursSpecification",
|
|
187
|
-
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
|
188
|
-
opens: hours.split("-")[0] || "09:00",
|
|
189
|
-
closes: hours.split("-")[1] || "18:00",
|
|
190
|
-
}))
|
|
191
|
-
: undefined,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* 3. SoftwareApplication & WebApplication
|
|
197
|
-
*/
|
|
198
|
-
static buildSoftwareApplication(opts: {
|
|
199
|
-
name: string;
|
|
200
|
-
description: string;
|
|
201
|
-
url: string;
|
|
202
|
-
priceMonthly?: number;
|
|
203
|
-
currency?: string;
|
|
204
|
-
operatingSystem?: string;
|
|
205
|
-
category?: string;
|
|
206
|
-
}): Record<string, unknown> {
|
|
207
|
-
return {
|
|
208
|
-
"@context": "https://schema.org",
|
|
209
|
-
"@type": "SoftwareApplication",
|
|
210
|
-
name: opts.name,
|
|
211
|
-
description: opts.description,
|
|
212
|
-
url: opts.url,
|
|
213
|
-
applicationCategory: opts.category || "BusinessApplication",
|
|
214
|
-
operatingSystem: opts.operatingSystem || "Web, Cloud, iOS, Android",
|
|
215
|
-
offers: {
|
|
216
|
-
"@type": "Offer",
|
|
217
|
-
price: opts.priceMonthly !== undefined ? String(opts.priceMonthly) : "0",
|
|
218
|
-
priceCurrency: opts.currency || "USD",
|
|
219
|
-
description: "14-day free trial, no credit card required",
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 4. FAQPage
|
|
226
|
-
*/
|
|
227
|
-
static buildFAQPage(faqs: { question: string; answer: string }[]): Record<string, unknown> {
|
|
228
|
-
return {
|
|
229
|
-
"@context": "https://schema.org",
|
|
230
|
-
"@type": "FAQPage",
|
|
231
|
-
mainEntity: faqs.map((faq) => ({
|
|
232
|
-
"@type": "Question",
|
|
233
|
-
name: faq.question,
|
|
234
|
-
acceptedAnswer: {
|
|
235
|
-
"@type": "Answer",
|
|
236
|
-
text: faq.answer,
|
|
237
|
-
},
|
|
238
|
-
})),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* 5. HowTo & HowToStep
|
|
244
|
-
*/
|
|
245
|
-
static buildHowTo(opts: {
|
|
246
|
-
name: string;
|
|
247
|
-
description: string;
|
|
248
|
-
steps: string[];
|
|
249
|
-
}): Record<string, unknown> {
|
|
250
|
-
return {
|
|
251
|
-
"@context": "https://schema.org",
|
|
252
|
-
"@type": "HowTo",
|
|
253
|
-
name: opts.name,
|
|
254
|
-
description: opts.description,
|
|
255
|
-
step: opts.steps.map((text, idx) => ({
|
|
256
|
-
"@type": "HowToStep",
|
|
257
|
-
position: idx + 1,
|
|
258
|
-
name: `Step ${idx + 1}`,
|
|
259
|
-
text,
|
|
260
|
-
})),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* 6. BreadcrumbList
|
|
266
|
-
*/
|
|
267
|
-
static buildBreadcrumbs(items: { name: string; url: string }[]): Record<string, unknown> {
|
|
268
|
-
return {
|
|
269
|
-
"@context": "https://schema.org",
|
|
270
|
-
"@type": "BreadcrumbList",
|
|
271
|
-
itemListElement: items.map((item, idx) => ({
|
|
272
|
-
"@type": "ListItem",
|
|
273
|
-
position: idx + 1,
|
|
274
|
-
name: item.name,
|
|
275
|
-
item: item.url,
|
|
276
|
-
})),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 7. AggregateRating (Google Gold Stars in SERPs) - Strictly Dynamic (No Fake Defaults)
|
|
282
|
-
*/
|
|
283
|
-
static buildAggregateRating(opts: {
|
|
284
|
-
ratingValue?: number;
|
|
285
|
-
reviewCount?: number;
|
|
286
|
-
bestRating?: number;
|
|
287
|
-
worstRating?: number;
|
|
288
|
-
itemReviewedName: string;
|
|
289
|
-
}): Record<string, unknown> | null {
|
|
290
|
-
// Google Strict Compliance: Never emit fake or default hardcoded reviews
|
|
291
|
-
if (!opts.ratingValue || !opts.reviewCount || opts.reviewCount <= 0) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
return {
|
|
295
|
-
"@type": "AggregateRating",
|
|
296
|
-
ratingValue: opts.ratingValue,
|
|
297
|
-
reviewCount: opts.reviewCount,
|
|
298
|
-
bestRating: opts.bestRating ?? 5,
|
|
299
|
-
worstRating: opts.worstRating ?? 1,
|
|
300
|
-
itemReviewed: {
|
|
301
|
-
"@type": "Thing",
|
|
302
|
-
name: opts.itemReviewedName,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* 8. AggregateOffer (Price Ranges & Tiers) - Inspired by santifer-irepair
|
|
309
|
-
*/
|
|
310
|
-
static buildAggregateOffer(opts: {
|
|
311
|
-
lowPrice: number;
|
|
312
|
-
highPrice: number;
|
|
313
|
-
currency: string;
|
|
314
|
-
offerCount?: number;
|
|
315
|
-
description?: string;
|
|
316
|
-
}): Record<string, unknown> {
|
|
317
|
-
return {
|
|
318
|
-
"@type": "AggregateOffer",
|
|
319
|
-
lowPrice: String(opts.lowPrice),
|
|
320
|
-
highPrice: String(opts.highPrice),
|
|
321
|
-
priceCurrency: opts.currency,
|
|
322
|
-
offerCount: String(opts.offerCount ?? 3),
|
|
323
|
-
description: opts.description || "Transparent pricing with zero lock-in",
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* 9. Service with ServiceArea & Providers
|
|
329
|
-
*/
|
|
330
|
-
static buildService(opts: {
|
|
331
|
-
name: string;
|
|
332
|
-
description: string;
|
|
333
|
-
providerName: string;
|
|
334
|
-
areaServed?: string[];
|
|
335
|
-
serviceType?: string;
|
|
336
|
-
}): Record<string, unknown> {
|
|
337
|
-
return {
|
|
338
|
-
"@context": "https://schema.org",
|
|
339
|
-
"@type": "Service",
|
|
340
|
-
name: opts.name,
|
|
341
|
-
description: opts.description,
|
|
342
|
-
serviceType: opts.serviceType || "SoftwareService",
|
|
343
|
-
provider: {
|
|
344
|
-
"@type": "Organization",
|
|
345
|
-
name: opts.providerName,
|
|
346
|
-
},
|
|
347
|
-
areaServed: opts.areaServed
|
|
348
|
-
? opts.areaServed.map((city) => ({
|
|
349
|
-
"@type": "City",
|
|
350
|
-
name: city,
|
|
351
|
-
}))
|
|
352
|
-
: undefined,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* 10. WebSite with Google Sitelinks SearchBox
|
|
358
|
-
*/
|
|
359
|
-
static buildWebSite(opts: {
|
|
360
|
-
name: string;
|
|
361
|
-
url: string;
|
|
362
|
-
searchUrlTemplate?: string;
|
|
363
|
-
}): Record<string, unknown> {
|
|
364
|
-
return {
|
|
365
|
-
"@context": "https://schema.org",
|
|
366
|
-
"@type": "WebSite",
|
|
367
|
-
name: opts.name,
|
|
368
|
-
url: opts.url,
|
|
369
|
-
potentialAction: opts.searchUrlTemplate
|
|
370
|
-
? {
|
|
371
|
-
"@type": "SearchAction",
|
|
372
|
-
target: {
|
|
373
|
-
"@type": "EntryPoint",
|
|
374
|
-
urlTemplate: opts.searchUrlTemplate,
|
|
375
|
-
},
|
|
376
|
-
"query-input": "required name=search_term_string",
|
|
377
|
-
}
|
|
378
|
-
: undefined,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* 11. Speakable Schema (Google Assistant & Voice Search) - Rank Math Pro Parity
|
|
384
|
-
*/
|
|
385
|
-
static buildSpeakable(opts: {
|
|
386
|
-
cssSelectors?: string[];
|
|
387
|
-
xpaths?: string[];
|
|
388
|
-
}): Record<string, unknown> {
|
|
389
|
-
return {
|
|
390
|
-
"@type": "SpeakableSpecification",
|
|
391
|
-
cssSelector: opts.cssSelectors || [".headline", ".summary", ".direct-answer", "article h1", "article p:first-of-type"],
|
|
392
|
-
xpath: opts.xpaths,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* 12. Podcast & PodcastEpisode Schema
|
|
398
|
-
*/
|
|
399
|
-
static buildPodcastEpisode(opts: {
|
|
400
|
-
title: string;
|
|
401
|
-
description: string;
|
|
402
|
-
audioUrl: string;
|
|
403
|
-
duration?: string;
|
|
404
|
-
publishedDate: string;
|
|
405
|
-
seriesName: string;
|
|
406
|
-
}): Record<string, unknown> {
|
|
407
|
-
return {
|
|
408
|
-
"@context": "https://schema.org",
|
|
409
|
-
"@type": "PodcastEpisode",
|
|
410
|
-
name: opts.title,
|
|
411
|
-
description: opts.description,
|
|
412
|
-
datePublished: opts.publishedDate,
|
|
413
|
-
timeRequired: opts.duration || "PT15M",
|
|
414
|
-
associatedMedia: {
|
|
415
|
-
"@type": "AudioObject",
|
|
416
|
-
contentUrl: opts.audioUrl,
|
|
417
|
-
},
|
|
418
|
-
partOfSeries: {
|
|
419
|
-
"@type": "PodcastSeries",
|
|
420
|
-
name: opts.seriesName,
|
|
421
|
-
},
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* 13. Restaurant Schema (with Menu & Cuisine)
|
|
427
|
-
*/
|
|
428
|
-
static buildRestaurant(opts: {
|
|
429
|
-
name: string;
|
|
430
|
-
url: string;
|
|
431
|
-
servesCuisine: string[];
|
|
432
|
-
menuUrl?: string;
|
|
433
|
-
priceRange?: string;
|
|
434
|
-
telephone?: string;
|
|
435
|
-
address: { street: string; city: string; postalCode: string; country: string };
|
|
436
|
-
}): Record<string, unknown> {
|
|
437
|
-
return {
|
|
438
|
-
"@context": "https://schema.org",
|
|
439
|
-
"@type": "Restaurant",
|
|
440
|
-
name: opts.name,
|
|
441
|
-
url: opts.url,
|
|
442
|
-
servesCuisine: opts.servesCuisine,
|
|
443
|
-
hasMenu: opts.menuUrl,
|
|
444
|
-
priceRange: opts.priceRange || "€€",
|
|
445
|
-
telephone: opts.telephone,
|
|
446
|
-
address: {
|
|
447
|
-
"@type": "PostalAddress",
|
|
448
|
-
streetAddress: opts.address.street,
|
|
449
|
-
addressLocality: opts.address.city,
|
|
450
|
-
postalCode: opts.address.postalCode,
|
|
451
|
-
addressCountry: opts.address.country,
|
|
452
|
-
},
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* 14. Book Schema
|
|
458
|
-
*/
|
|
459
|
-
static buildBook(opts: {
|
|
460
|
-
name: string;
|
|
461
|
-
author: string;
|
|
462
|
-
isbn?: string;
|
|
463
|
-
bookFormat?: string;
|
|
464
|
-
url?: string;
|
|
465
|
-
}): Record<string, unknown> {
|
|
466
|
-
return {
|
|
467
|
-
"@context": "https://schema.org",
|
|
468
|
-
"@type": "Book",
|
|
469
|
-
name: opts.name,
|
|
470
|
-
author: {
|
|
471
|
-
"@type": "Person",
|
|
472
|
-
name: opts.author,
|
|
473
|
-
},
|
|
474
|
-
isbn: opts.isbn,
|
|
475
|
-
bookFormat: opts.bookFormat || "https://schema.org/EBook",
|
|
476
|
-
url: opts.url,
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* 15. Dataset Schema (Data Search & Research)
|
|
482
|
-
*/
|
|
483
|
-
static buildDataset(opts: {
|
|
484
|
-
name: string;
|
|
485
|
-
description: string;
|
|
486
|
-
url: string;
|
|
487
|
-
keywords: string[];
|
|
488
|
-
temporalCoverage?: string;
|
|
489
|
-
distributionUrl?: string;
|
|
490
|
-
}): Record<string, unknown> {
|
|
491
|
-
return {
|
|
492
|
-
"@context": "https://schema.org",
|
|
493
|
-
"@type": "Dataset",
|
|
494
|
-
name: opts.name,
|
|
495
|
-
description: opts.description,
|
|
496
|
-
url: opts.url,
|
|
497
|
-
keywords: opts.keywords,
|
|
498
|
-
temporalCoverage: opts.temporalCoverage || "2024/2026",
|
|
499
|
-
distribution: opts.distributionUrl
|
|
500
|
-
? [
|
|
501
|
-
{
|
|
502
|
-
"@type": "DataDownload",
|
|
503
|
-
encodingFormat: "application/json",
|
|
504
|
-
contentUrl: opts.distributionUrl,
|
|
505
|
-
},
|
|
506
|
-
]
|
|
507
|
-
: undefined,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* 16. FactCheck Schema (Google ClaimReview)
|
|
513
|
-
*/
|
|
514
|
-
static buildFactCheck(opts: {
|
|
515
|
-
claimReviewed: string;
|
|
516
|
-
ratingValue: string;
|
|
517
|
-
authorName: string;
|
|
518
|
-
url: string;
|
|
519
|
-
}): Record<string, unknown> {
|
|
520
|
-
return {
|
|
521
|
-
"@context": "https://schema.org",
|
|
522
|
-
"@type": "ClaimReview",
|
|
523
|
-
claimReviewed: opts.claimReviewed,
|
|
524
|
-
url: opts.url,
|
|
525
|
-
reviewRating: {
|
|
526
|
-
"@type": "Rating",
|
|
527
|
-
ratingValue: opts.ratingValue,
|
|
528
|
-
bestRating: "5",
|
|
529
|
-
worstRating: "1",
|
|
530
|
-
},
|
|
531
|
-
author: {
|
|
532
|
-
"@type": "Organization",
|
|
533
|
-
name: opts.authorName,
|
|
534
|
-
},
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* 17. Carousel / ItemList Schema
|
|
540
|
-
*/
|
|
541
|
-
static buildCarousel(items: Array<{ name: string; url: string; position: number; image?: string }>): Record<string, unknown> {
|
|
542
|
-
return {
|
|
543
|
-
"@context": "https://schema.org",
|
|
544
|
-
"@type": "ItemList",
|
|
545
|
-
itemListElement: items.map((item) => ({
|
|
546
|
-
"@type": "ListItem",
|
|
547
|
-
position: item.position,
|
|
548
|
-
url: item.url,
|
|
549
|
-
name: item.name,
|
|
550
|
-
image: item.image,
|
|
551
|
-
})),
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* 18. QAPage Schema (bbPress / Forums / Community QA)
|
|
557
|
-
*/
|
|
558
|
-
static buildQAPage(opts: {
|
|
559
|
-
questionText: string;
|
|
560
|
-
authorName: string;
|
|
561
|
-
answerText: string;
|
|
562
|
-
answerAuthor: string;
|
|
563
|
-
upvoteCount?: number;
|
|
564
|
-
}): Record<string, unknown> {
|
|
565
|
-
return {
|
|
566
|
-
"@context": "https://schema.org",
|
|
567
|
-
"@type": "QAPage",
|
|
568
|
-
mainEntity: {
|
|
569
|
-
"@type": "Question",
|
|
570
|
-
name: opts.questionText,
|
|
571
|
-
text: opts.questionText,
|
|
572
|
-
author: {
|
|
573
|
-
"@type": "Person",
|
|
574
|
-
name: opts.authorName,
|
|
575
|
-
},
|
|
576
|
-
answerCount: 1,
|
|
577
|
-
acceptedAnswer: {
|
|
578
|
-
"@type": "Answer",
|
|
579
|
-
text: opts.answerText,
|
|
580
|
-
upvoteCount: opts.upvoteCount ?? 12,
|
|
581
|
-
author: {
|
|
582
|
-
"@type": "Person",
|
|
583
|
-
name: opts.answerAuthor,
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
}
|