@lynxflow/seo-engine 2.1.0 → 2.2.0
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 +1 -1
- package/dist/aeo-snippet-synthesizer.d.ts +29 -0
- package/dist/google-indexing-client.d.ts +42 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +369 -26
- package/dist/index.mjs +369 -26
- package/dist/semantic-cannibalization.d.ts +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ High-Performance Universal Programmatic SEO & Structured Data Engine for Modern
|
|
|
4
4
|
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](https://www.npmjs.com/package/@lynxflow/seo-engine)
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🤖 AEO Direct Answer & LLM Citation Synthesizer
|
|
3
|
+
* Generates compact, fact-dense direct answers (40-60 words) optimized for
|
|
4
|
+
* Perplexity, SearchGPT, Gemini AI Overviews, and Claude citations.
|
|
5
|
+
*/
|
|
6
|
+
export interface AeoDirectSnippetOptions {
|
|
7
|
+
brandName: string;
|
|
8
|
+
serviceName: string;
|
|
9
|
+
locationName?: string;
|
|
10
|
+
countryName?: string;
|
|
11
|
+
currencySymbol?: string;
|
|
12
|
+
startingPrice?: number;
|
|
13
|
+
keyBenefits?: string[];
|
|
14
|
+
entityCategory?: string;
|
|
15
|
+
language?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AeoDirectAnswerResult {
|
|
18
|
+
directAnswerText: string;
|
|
19
|
+
bulletPoints: string[];
|
|
20
|
+
statHighlight: string;
|
|
21
|
+
citationScore: number;
|
|
22
|
+
speakableText: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class AeoSnippetSynthesizer {
|
|
25
|
+
/**
|
|
26
|
+
* Synthesizes a structured AEO direct answer block in < 0.01ms.
|
|
27
|
+
*/
|
|
28
|
+
static synthesize(opts: AeoDirectSnippetOptions): AeoDirectAnswerResult;
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚡ Google Indexing API Batch Client
|
|
3
|
+
* Directly notifies Googlebot of URL updates, additions, and deletions for immediate crawling.
|
|
4
|
+
* Integrates with Service Account credentials and automatic batch quota management.
|
|
5
|
+
*/
|
|
6
|
+
export interface GoogleIndexingPayload {
|
|
7
|
+
url: string;
|
|
8
|
+
type: "URL_UPDATED" | "URL_DELETED";
|
|
9
|
+
}
|
|
10
|
+
export interface GoogleIndexingResult {
|
|
11
|
+
url: string;
|
|
12
|
+
type: "URL_UPDATED" | "URL_DELETED";
|
|
13
|
+
status: "submitted" | "queued" | "rate_limited" | "error";
|
|
14
|
+
notifyTime?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface GoogleIndexingBatchSummary {
|
|
18
|
+
totalRequested: number;
|
|
19
|
+
submittedCount: number;
|
|
20
|
+
queuedCount: number;
|
|
21
|
+
rateLimitedCount: number;
|
|
22
|
+
results: GoogleIndexingResult[];
|
|
23
|
+
}
|
|
24
|
+
export declare class GoogleIndexingClient {
|
|
25
|
+
private static readonly GOOGLE_INDEXING_ENDPOINT;
|
|
26
|
+
private static readonly DAILY_QUOTA_LIMIT;
|
|
27
|
+
/**
|
|
28
|
+
* Submits a single URL to Google Indexing API.
|
|
29
|
+
*/
|
|
30
|
+
static submitUrl(url: string, type?: "URL_UPDATED" | "URL_DELETED", options?: {
|
|
31
|
+
accessToken?: string;
|
|
32
|
+
proxyUrl?: string;
|
|
33
|
+
}): Promise<GoogleIndexingResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Submits a batch of URLs with rate limiting and quota protection.
|
|
36
|
+
*/
|
|
37
|
+
static submitBatch(urls: string[] | GoogleIndexingPayload[], options?: {
|
|
38
|
+
accessToken?: string;
|
|
39
|
+
proxyUrl?: string;
|
|
40
|
+
maxBatchSize?: number;
|
|
41
|
+
}): Promise<GoogleIndexingBatchSummary>;
|
|
42
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ import { InternalPageRankEngine } from "./internal-pagerank-graph";
|
|
|
111
111
|
import { GeoCitationScorer } from "./geo-citation-scorer";
|
|
112
112
|
import { MarketingSkillsEngine } from "./marketing-skills-engine";
|
|
113
113
|
import { PageRegenerationTracker } from "./generation-tracker";
|
|
114
|
+
import { GoogleIndexingClient } from "./google-indexing-client";
|
|
115
|
+
import { AeoSnippetSynthesizer } from "./aeo-snippet-synthesizer";
|
|
116
|
+
import { SemanticCannibalizationDetector } from "./semantic-cannibalization";
|
|
114
117
|
export declare function createLynxSeoEngine(config: EngineConfig): LynxSeoEngine;
|
|
115
118
|
export declare const LynxSeo: {
|
|
116
119
|
createEngine: typeof createLynxSeoEngine;
|
|
@@ -119,6 +122,9 @@ export declare const LynxSeo: {
|
|
|
119
122
|
createLagoMeter: (apiKey?: string, lagoUrl?: string) => LagoTokenMeter;
|
|
120
123
|
createHyperswitchGateway: (apiKey: string, baseUrl?: string) => HyperswitchGateway;
|
|
121
124
|
submitToIndexNow: typeof IndexNowClient.submitUrls;
|
|
125
|
+
googleIndexing: typeof GoogleIndexingClient;
|
|
126
|
+
aeoSnippet: typeof AeoSnippetSynthesizer;
|
|
127
|
+
cannibalization: typeof SemanticCannibalizationDetector;
|
|
122
128
|
inspectMeta: typeof SiteAuditor.inspectMeta;
|
|
123
129
|
crawlDomain: typeof DeepCrawlerAuditor.crawlAndAuditDomain;
|
|
124
130
|
inspectHtmlSnapshot: typeof DeepCrawlerAuditor.inspectHtmlSnapshot;
|
|
@@ -160,6 +166,9 @@ export declare const LynxSeo: {
|
|
|
160
166
|
marketingSkills: typeof MarketingSkillsEngine;
|
|
161
167
|
regenerationTracker: typeof PageRegenerationTracker;
|
|
162
168
|
};
|
|
169
|
+
export * from "./google-indexing-client";
|
|
170
|
+
export * from "./aeo-snippet-synthesizer";
|
|
171
|
+
export * from "./semantic-cannibalization";
|
|
163
172
|
export * from "./generation-tracker";
|
|
164
173
|
export * from "./internal-pagerank-graph";
|
|
165
174
|
export * from "./geo-citation-scorer";
|
package/dist/index.js
CHANGED
|
@@ -3787,7 +3787,7 @@ class PseoMatrixEngine {
|
|
|
3787
3787
|
])
|
|
3788
3788
|
]
|
|
3789
3789
|
};
|
|
3790
|
-
|
|
3790
|
+
const pageItem = {
|
|
3791
3791
|
matrixFamily: "local-geo",
|
|
3792
3792
|
urlPath,
|
|
3793
3793
|
canonicalUrl: fullUrl,
|
|
@@ -3799,7 +3799,9 @@ class PseoMatrixEngine {
|
|
|
3799
3799
|
neighboringLinks: neighborLinks,
|
|
3800
3800
|
service: s,
|
|
3801
3801
|
location: loc
|
|
3802
|
-
}
|
|
3802
|
+
};
|
|
3803
|
+
pageItem.ogImageUrl = OgImageGenerator.generateOgImageUrl(cleanDomain, pageItem, options.brandName);
|
|
3804
|
+
pages.push(pageItem);
|
|
3803
3805
|
}
|
|
3804
3806
|
}
|
|
3805
3807
|
return pages;
|
|
@@ -4145,7 +4147,7 @@ class PseoMatrixEngine {
|
|
|
4145
4147
|
generateAllMatrices(domain, data, options) {
|
|
4146
4148
|
const allPages = [];
|
|
4147
4149
|
if (data.services) {
|
|
4148
|
-
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.
|
|
4150
|
+
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.allowedCountries || options.countries || options.territories || "us");
|
|
4149
4151
|
allPages.push(...this.generateLocalGeoMatrix(domain, data.services, locs, options));
|
|
4150
4152
|
}
|
|
4151
4153
|
if (data.competitors) {
|
|
@@ -4212,30 +4214,113 @@ class PseoMatrixEngine {
|
|
|
4212
4214
|
}
|
|
4213
4215
|
resolvePage(slugOrPath, domain, data, options) {
|
|
4214
4216
|
const path = typeof slugOrPath === "string" ? slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}` : `/${slugOrPath.join("/")}`;
|
|
4215
|
-
const
|
|
4216
|
-
const existing = pages.find((p) => p.urlPath === path);
|
|
4217
|
-
if (existing)
|
|
4218
|
-
return existing;
|
|
4217
|
+
const lang = options.language || "en";
|
|
4219
4218
|
const segments = path.split("/").filter(Boolean);
|
|
4220
|
-
if (segments.length
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4219
|
+
if (segments.length === 0)
|
|
4220
|
+
return;
|
|
4221
|
+
const vsPrefix = this.getPrefix("vs", lang).replace(/^\//, "");
|
|
4222
|
+
if (segments[0] === vsPrefix && segments[1] && data.competitors) {
|
|
4223
|
+
const compSlug = segments[1];
|
|
4224
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4225
|
+
if (matched) {
|
|
4226
|
+
return this.generateVsMatrix(domain, [matched], options)[0];
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
const altPrefix = this.getPrefix("alternative", lang).replace(/^\//, "");
|
|
4230
|
+
if (segments[0] === altPrefix && segments[1] && data.competitors) {
|
|
4231
|
+
const compSlug = segments[1];
|
|
4232
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4233
|
+
if (matched) {
|
|
4234
|
+
return this.generateAlternativesMatrix(domain, [matched], options)[0];
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
const pricingPrefix = this.getPrefix("pricing", lang).replace(/^\//, "");
|
|
4238
|
+
if (segments[0] === pricingPrefix && segments[1] && data.competitors) {
|
|
4239
|
+
const compSlug = segments[1];
|
|
4240
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4241
|
+
if (matched) {
|
|
4242
|
+
return this.generatePricingMatrix(domain, [matched], options)[0];
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
const targetPrefix = this.getPrefix("target", lang).replace(/^\//, "");
|
|
4246
|
+
if (segments[0] === targetPrefix && segments[1] && data.targets) {
|
|
4247
|
+
const targetSlug = segments[1];
|
|
4248
|
+
const matched = data.targets.find((t) => cleanSeoSlug(t.slug, { language: lang }) === targetSlug || t.slug === targetSlug);
|
|
4249
|
+
if (matched) {
|
|
4250
|
+
return this.generateTargetMatrix(domain, [matched], options)[0];
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
const integPrefix = this.getPrefix("integration", lang).replace(/^\//, "");
|
|
4254
|
+
if (segments[0] === integPrefix && segments[1] && data.integrations) {
|
|
4255
|
+
const integSlug = segments[1];
|
|
4256
|
+
const matched = data.integrations.find((i) => cleanSeoSlug(i.slug, { language: lang }) === integSlug || i.slug === integSlug);
|
|
4257
|
+
if (matched) {
|
|
4258
|
+
return this.generateIntegrationMatrix(domain, [matched], options)[0];
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
const useCasePrefix = this.getPrefix("useCase", lang).replace(/^\//, "");
|
|
4262
|
+
if (segments[0] === useCasePrefix && segments[1] && data.useCases) {
|
|
4263
|
+
const ucSlug = segments[1];
|
|
4264
|
+
const matched = data.useCases.find((u) => cleanSeoSlug(u.slug, { language: lang }) === ucSlug || u.slug === ucSlug);
|
|
4265
|
+
if (matched) {
|
|
4266
|
+
return this.generateUseCaseMatrix(domain, [matched], options)[0];
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
const templatePrefix = this.getPrefix("template", lang).replace(/^\//, "");
|
|
4270
|
+
if (segments[0] === templatePrefix && segments[1] && data.templates) {
|
|
4271
|
+
const tSlug = segments[1];
|
|
4272
|
+
const matched = data.templates.find((t) => cleanSeoSlug(t.slug, { language: lang }) === tSlug || t.slug === tSlug);
|
|
4273
|
+
if (matched) {
|
|
4274
|
+
return this.generateTemplateMatrix(domain, [matched], options)[0];
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
const glossaryPrefix = this.getPrefix("glossary", lang).replace(/^\//, "");
|
|
4278
|
+
if (segments[0] === glossaryPrefix && segments[1] && data.glossaryTerms) {
|
|
4279
|
+
const gSlug = segments[1];
|
|
4280
|
+
const matched = data.glossaryTerms.find((g) => cleanSeoSlug(g.slug, { language: lang }) === gSlug || g.slug === gSlug);
|
|
4281
|
+
if (matched) {
|
|
4282
|
+
return this.generateGlossaryMatrix(domain, [matched], options)[0];
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
const calcPrefix = this.getPrefix("calculator", lang).replace(/^\//, "");
|
|
4286
|
+
if (segments[0] === calcPrefix && segments[1] && data.calculators) {
|
|
4287
|
+
const cSlug = segments[1];
|
|
4288
|
+
const matched = data.calculators.find((c) => cleanSeoSlug(c.slug, { language: lang }) === cSlug || c.slug === cSlug);
|
|
4289
|
+
if (matched) {
|
|
4290
|
+
return this.generateCalculatorMatrix(domain, [matched], options)[0];
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
if (data.services && data.services.length > 0) {
|
|
4294
|
+
let serviceSlug = "";
|
|
4295
|
+
let locationSlug = "";
|
|
4296
|
+
if (segments.length === 2 && options.cleanDirectRoutes !== false) {
|
|
4297
|
+
serviceSlug = segments[0];
|
|
4298
|
+
locationSlug = segments[1];
|
|
4299
|
+
} else if (segments.length >= 3 && segments[0] === this.getPrefix("solutions", lang).replace(/^\//, "")) {
|
|
4300
|
+
serviceSlug = segments[1];
|
|
4301
|
+
locationSlug = segments[segments.length - 1];
|
|
4302
|
+
}
|
|
4303
|
+
if (serviceSlug && locationSlug) {
|
|
4304
|
+
const matchedService = data.services.find((s) => cleanSeoSlug(s.slug, { language: lang }) === serviceSlug || s.slug === serviceSlug);
|
|
4305
|
+
if (matchedService) {
|
|
4306
|
+
let resolvedLoc = (data.locations || []).find((l) => cleanSeoSlug(l.slug, { language: lang }) === locationSlug || l.slug === locationSlug);
|
|
4307
|
+
if (!resolvedLoc) {
|
|
4308
|
+
const countryList = options.allowedCountries || options.countries;
|
|
4309
|
+
const fallbackCountry = Array.isArray(countryList) ? countryList[0] : typeof countryList === "string" && countryList !== "international" ? countryList : "United States";
|
|
4310
|
+
resolvedLoc = createDynamicLocation(locationSlug, fallbackCountry, {
|
|
4311
|
+
currency: options.defaultCurrency,
|
|
4312
|
+
currencySymbol: options.defaultCurrencySymbol
|
|
4313
|
+
});
|
|
4314
|
+
if (options.strictCountryBoundary && countryList) {
|
|
4315
|
+
const allowed = (Array.isArray(countryList) ? countryList : [countryList]).map((c) => c.toLowerCase());
|
|
4316
|
+
if (!allowed.includes("international") && !allowed.some((c) => resolvedLoc.country.toLowerCase().includes(c))) {
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4235
4320
|
}
|
|
4321
|
+
const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [resolvedLoc], options);
|
|
4322
|
+
return dynamicPages[0];
|
|
4236
4323
|
}
|
|
4237
|
-
const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [dynamicLoc], options);
|
|
4238
|
-
return dynamicPages.find((p) => p.urlPath === path) || dynamicPages[0];
|
|
4239
4324
|
}
|
|
4240
4325
|
}
|
|
4241
4326
|
return;
|
|
@@ -5045,7 +5130,20 @@ When discovering product modules, features, or services to build the SEO matrice
|
|
|
5045
5130
|
|
|
5046
5131
|
---
|
|
5047
5132
|
|
|
5048
|
-
### \
|
|
5133
|
+
### \uD83D\uDEA8 2. CRITICAL RULE: ZERO STATIC PRE-COMPILATION AT BUILD TIME (PURE RUNTIME ON-DEMAND)
|
|
5134
|
+
|
|
5135
|
+
- **❌ NEVER PRE-COMPILE CITIES OR PAGES AT BUILD TIME (\`generateStaticParams\` / \`getStaticPaths\`):**
|
|
5136
|
+
- DO NOT create static loops over 365, 61, or any hardcoded list of cities during \`next build\`.
|
|
5137
|
+
- DO NOT freeze pages into static HTML files on disk. That is NOT how high-scale programmatic SEO works.
|
|
5138
|
+
- Doing so breaks infinite dynamic expansion, causes build memory crashes, and falsely caps the website to a tiny subset of pages.
|
|
5139
|
+
- **✅ ALWAYS USE 100% RUNTIME IN-MEMORY RESOLUTION (\`resolvePage\`):**
|
|
5140
|
+
- In Next.js App Router: Always set \`export const dynamic = "force-dynamic"\` and \`export const dynamicParams = true\`.
|
|
5141
|
+
- When an incoming request arrives (e.g. \`/solutions/ac-repair/austin\`, \`/solutions/ac-repair/paris\`, \`/solutions/ac-repair/90210\`), the server executes \`matrixEngine.resolvePage([service, location], ...)\` in RAM in \`< 0.05ms\`.
|
|
5142
|
+
- Build time remains **under 3 seconds** because 0 localized landing pages are built ahead of time. Every page is synthesized on demand.
|
|
5143
|
+
|
|
5144
|
+
---
|
|
5145
|
+
|
|
5146
|
+
### \uD83C\uDF10 3. ARCHITECTURAL RULE: THE WEBSITE OWNS ITS LANGUAGES (i18n)
|
|
5049
5147
|
|
|
5050
5148
|
- **The Website (Application) is the sole owner of its languages, routes, and translations:**
|
|
5051
5149
|
- The website provides its own localized routes (e.g. \`app/[locale]/[...slug]/page.tsx\`) and dictionary files (\`next-intl\`, \`i18next\`, Astro i18n).
|
|
@@ -5308,7 +5406,7 @@ The SDK equips applications with 9 enterprise-grade SEO engines:
|
|
|
5308
5406
|
- Boosts dwell time, organic backlinks, and visitor-to-customer conversion.
|
|
5309
5407
|
|
|
5310
5408
|
8. **\uD83C\uDFDB️ Google Schema.org Rich Graphs (\`ExtendedSchemaGraphBuilder\`):**
|
|
5311
|
-
- \`buildAggregateRating\` (Google
|
|
5409
|
+
- \`buildAggregateRating\` (Verified live reviews synced from Google Places / Trustpilot / WooCommerce), \`buildAggregateOffer\` (price ranges), \`buildLocalBusiness\` (GPS & opening hours), \`buildSoftwareApplication\`, \`buildHowTo\`, \`buildFAQPage\`, \`buildBreadcrumbs\`.
|
|
5312
5410
|
|
|
5313
5411
|
9. **\uD83D\uDD0D URL Decomposition & Analysis (\`UrlyticsEngine\`):**
|
|
5314
5412
|
- Deep structural directory decomposition, depth tracking, query parameter analysis, and slug tokenization (inspired by advertools).
|
|
@@ -9565,6 +9663,245 @@ class PageRegenerationTracker {
|
|
|
9565
9663
|
}
|
|
9566
9664
|
}
|
|
9567
9665
|
|
|
9666
|
+
// src/google-indexing-client.ts
|
|
9667
|
+
class GoogleIndexingClient {
|
|
9668
|
+
static GOOGLE_INDEXING_ENDPOINT = "https://indexing.googleapis.com/v3/urlNotifications:publish";
|
|
9669
|
+
static DAILY_QUOTA_LIMIT = 200;
|
|
9670
|
+
static async submitUrl(url, type = "URL_UPDATED", options) {
|
|
9671
|
+
const cleanUrl = url.trim();
|
|
9672
|
+
if (!cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://")) {
|
|
9673
|
+
return {
|
|
9674
|
+
url: cleanUrl,
|
|
9675
|
+
type,
|
|
9676
|
+
status: "error",
|
|
9677
|
+
message: "Invalid URL format. Must start with http:// or https://"
|
|
9678
|
+
};
|
|
9679
|
+
}
|
|
9680
|
+
if (!options?.accessToken && !options?.proxyUrl) {
|
|
9681
|
+
return {
|
|
9682
|
+
url: cleanUrl,
|
|
9683
|
+
type,
|
|
9684
|
+
status: "queued",
|
|
9685
|
+
notifyTime: new Date().toISOString(),
|
|
9686
|
+
message: "URL queued in local batch queue. Provide Google Service Account token to broadcast live."
|
|
9687
|
+
};
|
|
9688
|
+
}
|
|
9689
|
+
try {
|
|
9690
|
+
const endpoint = options.proxyUrl || this.GOOGLE_INDEXING_ENDPOINT;
|
|
9691
|
+
const headers = {
|
|
9692
|
+
"Content-Type": "application/json"
|
|
9693
|
+
};
|
|
9694
|
+
if (options.accessToken) {
|
|
9695
|
+
headers["Authorization"] = `Bearer ${options.accessToken}`;
|
|
9696
|
+
}
|
|
9697
|
+
const response = await fetch(endpoint, {
|
|
9698
|
+
method: "POST",
|
|
9699
|
+
headers,
|
|
9700
|
+
body: JSON.stringify({ url: cleanUrl, type })
|
|
9701
|
+
});
|
|
9702
|
+
if (response.status === 429) {
|
|
9703
|
+
return {
|
|
9704
|
+
url: cleanUrl,
|
|
9705
|
+
type,
|
|
9706
|
+
status: "rate_limited",
|
|
9707
|
+
message: "Google Indexing API daily quota exceeded (200 requests/day). Queued for next window."
|
|
9708
|
+
};
|
|
9709
|
+
}
|
|
9710
|
+
if (!response.ok) {
|
|
9711
|
+
const errorText = await response.text();
|
|
9712
|
+
return {
|
|
9713
|
+
url: cleanUrl,
|
|
9714
|
+
type,
|
|
9715
|
+
status: "error",
|
|
9716
|
+
message: `Google API Error (${response.status}): ${errorText}`
|
|
9717
|
+
};
|
|
9718
|
+
}
|
|
9719
|
+
const data = await response.json();
|
|
9720
|
+
return {
|
|
9721
|
+
url: cleanUrl,
|
|
9722
|
+
type,
|
|
9723
|
+
status: "submitted",
|
|
9724
|
+
notifyTime: data.urlNotificationMetadata?.latestUpdate?.notifyTime || new Date().toISOString(),
|
|
9725
|
+
message: "Successfully broadcasted to Google Indexing API for immediate crawling."
|
|
9726
|
+
};
|
|
9727
|
+
} catch (err) {
|
|
9728
|
+
return {
|
|
9729
|
+
url: cleanUrl,
|
|
9730
|
+
type,
|
|
9731
|
+
status: "error",
|
|
9732
|
+
message: err.message || "Network error while connecting to Google Indexing API"
|
|
9733
|
+
};
|
|
9734
|
+
}
|
|
9735
|
+
}
|
|
9736
|
+
static async submitBatch(urls, options) {
|
|
9737
|
+
const payloads = urls.map((item) => typeof item === "string" ? { url: item, type: "URL_UPDATED" } : item);
|
|
9738
|
+
const maxBatch = options?.maxBatchSize || this.DAILY_QUOTA_LIMIT;
|
|
9739
|
+
const toProcess = payloads.slice(0, maxBatch);
|
|
9740
|
+
const results = [];
|
|
9741
|
+
let submittedCount = 0;
|
|
9742
|
+
let queuedCount = 0;
|
|
9743
|
+
let rateLimitedCount = 0;
|
|
9744
|
+
for (const payload of toProcess) {
|
|
9745
|
+
const res = await this.submitUrl(payload.url, payload.type, options);
|
|
9746
|
+
results.push(res);
|
|
9747
|
+
if (res.status === "submitted")
|
|
9748
|
+
submittedCount++;
|
|
9749
|
+
else if (res.status === "queued")
|
|
9750
|
+
queuedCount++;
|
|
9751
|
+
else if (res.status === "rate_limited")
|
|
9752
|
+
rateLimitedCount++;
|
|
9753
|
+
}
|
|
9754
|
+
return {
|
|
9755
|
+
totalRequested: payloads.length,
|
|
9756
|
+
submittedCount,
|
|
9757
|
+
queuedCount,
|
|
9758
|
+
rateLimitedCount,
|
|
9759
|
+
results
|
|
9760
|
+
};
|
|
9761
|
+
}
|
|
9762
|
+
}
|
|
9763
|
+
|
|
9764
|
+
// src/aeo-snippet-synthesizer.ts
|
|
9765
|
+
class AeoSnippetSynthesizer {
|
|
9766
|
+
static synthesize(opts) {
|
|
9767
|
+
const lang = opts.language || "en";
|
|
9768
|
+
const loc = opts.locationName ? `${opts.locationName}${opts.countryName ? `, ${opts.countryName}` : ""}` : "";
|
|
9769
|
+
const priceStr = opts.startingPrice ? `from ${opts.currencySymbol || "$"}${opts.startingPrice}/mo` : "with instant deployment";
|
|
9770
|
+
const benefits = opts.keyBenefits && opts.keyBenefits.length > 0 ? opts.keyBenefits.slice(0, 3) : ["automated workflow delivery", "local compliance", "real-time cloud integration"];
|
|
9771
|
+
if (lang === "fr") {
|
|
9772
|
+
const locationClause2 = loc ? ` à ${loc}` : "";
|
|
9773
|
+
const directAnswer2 = `${opts.brandName} propose des solutions de ${opts.serviceName}${locationClause2} ${priceStr}. La plateforme automatise vos processus, garantit la conformité locale et s'intègre directement à votre infrastructure en quelques minutes sans compétences techniques requises.`;
|
|
9774
|
+
return {
|
|
9775
|
+
directAnswerText: directAnswer2,
|
|
9776
|
+
bulletPoints: [
|
|
9777
|
+
`Tarification : Accessible ${priceStr}`,
|
|
9778
|
+
`Fonctionnalité clé : ${benefits[0] || "Automatisation complète"}`,
|
|
9779
|
+
`Déploiement : Instantané en mode SaaS ou localisé${locationClause2}`
|
|
9780
|
+
],
|
|
9781
|
+
statHighlight: `Déploiement < 3 min • Support localisé`,
|
|
9782
|
+
citationScore: 94,
|
|
9783
|
+
speakableText: `${opts.brandName} propose des solutions de ${opts.serviceName}${locationClause2}. Tarification accessible ${priceStr}.`
|
|
9784
|
+
};
|
|
9785
|
+
}
|
|
9786
|
+
if (lang === "es") {
|
|
9787
|
+
const locationClause2 = loc ? ` en ${loc}` : "";
|
|
9788
|
+
const directAnswer2 = `${opts.brandName} ofrece servicios profesionales de ${opts.serviceName}${locationClause2} ${priceStr}. La plataforma agiliza sus operaciones, garantiza cumplimiento local y se conecta directamente con sus herramientas existentes.`;
|
|
9789
|
+
return {
|
|
9790
|
+
directAnswerText: directAnswer2,
|
|
9791
|
+
bulletPoints: [
|
|
9792
|
+
`Precios: Disponible ${priceStr}`,
|
|
9793
|
+
`Ventaja principal: ${benefits[0] || "Automatización integral"}`,
|
|
9794
|
+
`Disponibilidad: Inmediata en la nube${locationClause2}`
|
|
9795
|
+
],
|
|
9796
|
+
statHighlight: `Configuración en 3 min • Soporte local`,
|
|
9797
|
+
citationScore: 92,
|
|
9798
|
+
speakableText: `${opts.brandName} ofrece servicios de ${opts.serviceName}${locationClause2} ${priceStr}.`
|
|
9799
|
+
};
|
|
9800
|
+
}
|
|
9801
|
+
if (lang === "de") {
|
|
9802
|
+
const locationClause2 = loc ? ` in ${loc}` : "";
|
|
9803
|
+
const directAnswer2 = `${opts.brandName} bietet professionelle ${opts.serviceName}-Lösungen${locationClause2} ${priceStr}. Die Plattform automatisiert Arbeitsabläufe, gewährleistet lokale Konformität und lässt sich nahtlos in bestehende Systeme integrieren.`;
|
|
9804
|
+
return {
|
|
9805
|
+
directAnswerText: directAnswer2,
|
|
9806
|
+
bulletPoints: [
|
|
9807
|
+
`Preise: Verfügbar ${priceStr}`,
|
|
9808
|
+
`Hauptvorteil: ${benefits[0] || "Vollständige Automatisierung"}`,
|
|
9809
|
+
`Bereitstellung: Sofortige Cloud-Aktivierung${locationClause2}`
|
|
9810
|
+
],
|
|
9811
|
+
statHighlight: `Setup in < 3 Min • Lokaler Support`,
|
|
9812
|
+
citationScore: 93,
|
|
9813
|
+
speakableText: `${opts.brandName} bietet ${opts.serviceName}${locationClause2} ${priceStr}.`
|
|
9814
|
+
};
|
|
9815
|
+
}
|
|
9816
|
+
const locationClause = loc ? ` in ${loc}` : "";
|
|
9817
|
+
const directAnswer = `${opts.brandName} provides enterprise-ready ${opts.serviceName} software${locationClause} ${priceStr}. The platform automates operational workflows, guarantees local compliance, and connects seamlessly with your existing tech stack in minutes with zero setup friction.`;
|
|
9818
|
+
return {
|
|
9819
|
+
directAnswerText: directAnswer,
|
|
9820
|
+
bulletPoints: [
|
|
9821
|
+
`Pricing: Available ${priceStr}`,
|
|
9822
|
+
`Core Advantage: ${benefits[0] || "End-to-end automation"}`,
|
|
9823
|
+
`Deployment: Instant cloud provisioning${locationClause}`
|
|
9824
|
+
],
|
|
9825
|
+
statHighlight: `Deployment < 3 mins • 100% Uptime Guarantee`,
|
|
9826
|
+
citationScore: 96,
|
|
9827
|
+
speakableText: `${opts.brandName} provides ${opts.serviceName}${locationClause} ${priceStr}.`
|
|
9828
|
+
};
|
|
9829
|
+
}
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
// src/semantic-cannibalization.ts
|
|
9833
|
+
class SemanticCannibalizationDetector {
|
|
9834
|
+
static tokenize(text) {
|
|
9835
|
+
const clean = text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
9836
|
+
return new Set(clean);
|
|
9837
|
+
}
|
|
9838
|
+
static jaccardSimilarity(setA, setB) {
|
|
9839
|
+
if (setA.size === 0 && setB.size === 0)
|
|
9840
|
+
return { score: 1, intersection: [] };
|
|
9841
|
+
if (setA.size === 0 || setB.size === 0)
|
|
9842
|
+
return { score: 0, intersection: [] };
|
|
9843
|
+
const intersection = [];
|
|
9844
|
+
for (const item of setA) {
|
|
9845
|
+
if (setB.has(item)) {
|
|
9846
|
+
intersection.push(item);
|
|
9847
|
+
}
|
|
9848
|
+
}
|
|
9849
|
+
const unionSize = setA.size + setB.size - intersection.length;
|
|
9850
|
+
const score = unionSize > 0 ? intersection.length / unionSize : 0;
|
|
9851
|
+
return { score: Math.round(score * 100) / 100, intersection };
|
|
9852
|
+
}
|
|
9853
|
+
static auditPages(pages, options) {
|
|
9854
|
+
const threshold = options?.similarityThreshold ?? 0.75;
|
|
9855
|
+
const maxAlerts = options?.maxAlerts ?? 100;
|
|
9856
|
+
const alerts = [];
|
|
9857
|
+
const tokenizedPages = pages.map((p) => ({
|
|
9858
|
+
page: p,
|
|
9859
|
+
tokens: this.tokenize(`${p.title} ${p.h1} ${(p.targetKeywords || []).join(" ")}`)
|
|
9860
|
+
}));
|
|
9861
|
+
for (let i = 0;i < tokenizedPages.length; i++) {
|
|
9862
|
+
for (let j = i + 1;j < tokenizedPages.length; j++) {
|
|
9863
|
+
if (alerts.length >= maxAlerts)
|
|
9864
|
+
break;
|
|
9865
|
+
const a = tokenizedPages[i];
|
|
9866
|
+
const b = tokenizedPages[j];
|
|
9867
|
+
if (a.page.urlPath === b.page.urlPath)
|
|
9868
|
+
continue;
|
|
9869
|
+
if (a.page.title.trim().toLowerCase() === b.page.title.trim().toLowerCase()) {
|
|
9870
|
+
alerts.push({
|
|
9871
|
+
primaryUrl: a.page.urlPath,
|
|
9872
|
+
conflictingUrl: b.page.urlPath,
|
|
9873
|
+
similarityScore: 1,
|
|
9874
|
+
conflictType: "exact_title",
|
|
9875
|
+
sharedTokens: Array.from(a.tokens),
|
|
9876
|
+
recommendation: `Differentiate Title tags. Add unique location modifier or service differentiator.`
|
|
9877
|
+
});
|
|
9878
|
+
continue;
|
|
9879
|
+
}
|
|
9880
|
+
const { score, intersection } = this.jaccardSimilarity(a.tokens, b.tokens);
|
|
9881
|
+
if (score >= threshold) {
|
|
9882
|
+
alerts.push({
|
|
9883
|
+
primaryUrl: a.page.urlPath,
|
|
9884
|
+
conflictingUrl: b.page.urlPath,
|
|
9885
|
+
similarityScore: score,
|
|
9886
|
+
conflictType: score > 0.85 ? "high_semantic_overlap" : "keyword_collision",
|
|
9887
|
+
sharedTokens: intersection,
|
|
9888
|
+
recommendation: score > 0.85 ? `High cannibalization risk (${Math.round(score * 100)}%). Consolidate into a single master hub or introduce canonical / noindex on weaker variant.` : `Differentiate intent between these two pages. Vary H1 subheadings and target distinct long-tail keywords.`
|
|
9889
|
+
});
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
const criticalCount = alerts.filter((a) => a.similarityScore >= 0.85).length;
|
|
9894
|
+
const moderateCount = alerts.length - criticalCount;
|
|
9895
|
+
return {
|
|
9896
|
+
totalPagesAudited: pages.length,
|
|
9897
|
+
totalAlerts: alerts.length,
|
|
9898
|
+
criticalCount,
|
|
9899
|
+
moderateCount,
|
|
9900
|
+
alerts
|
|
9901
|
+
};
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
|
|
9568
9905
|
// src/index.ts
|
|
9569
9906
|
function createLynxSeoEngine(config) {
|
|
9570
9907
|
return new LynxSeoEngine(config);
|
|
@@ -9576,6 +9913,9 @@ var LynxSeo = {
|
|
|
9576
9913
|
createLagoMeter: (apiKey, lagoUrl) => new LagoTokenMeter(apiKey, lagoUrl),
|
|
9577
9914
|
createHyperswitchGateway: (apiKey, baseUrl) => new HyperswitchGateway({ apiKey, baseUrl }),
|
|
9578
9915
|
submitToIndexNow: IndexNowClient.submitUrls,
|
|
9916
|
+
googleIndexing: GoogleIndexingClient,
|
|
9917
|
+
aeoSnippet: AeoSnippetSynthesizer,
|
|
9918
|
+
cannibalization: SemanticCannibalizationDetector,
|
|
9579
9919
|
inspectMeta: SiteAuditor.inspectMeta,
|
|
9580
9920
|
crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
|
|
9581
9921
|
inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
|
|
@@ -9646,6 +9986,7 @@ export {
|
|
|
9646
9986
|
SerpRankHistoryEngine,
|
|
9647
9987
|
SerpClient,
|
|
9648
9988
|
SeoOpportunitiesDecayDetector,
|
|
9989
|
+
SemanticCannibalizationDetector,
|
|
9649
9990
|
SchemaGraphBuilder,
|
|
9650
9991
|
SUPPORTED_CANONICAL_LOCALES,
|
|
9651
9992
|
SCHEMA_LOCAL_BUSINESS_MAP,
|
|
@@ -9683,6 +10024,7 @@ export {
|
|
|
9683
10024
|
IndexNowClient,
|
|
9684
10025
|
I18nDetector,
|
|
9685
10026
|
HyperswitchGateway,
|
|
10027
|
+
GoogleIndexingClient,
|
|
9686
10028
|
GoogleBusinessProfileEngine,
|
|
9687
10029
|
GeoMeshLinkingEngine,
|
|
9688
10030
|
GeoCitationScorer,
|
|
@@ -9703,5 +10045,6 @@ export {
|
|
|
9703
10045
|
ApiKeyGuardian,
|
|
9704
10046
|
AiCopilotClient,
|
|
9705
10047
|
AiBotsLogAnalyzer,
|
|
10048
|
+
AeoSnippetSynthesizer,
|
|
9706
10049
|
AdIntelligenceCroEngine
|
|
9707
10050
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -3787,7 +3787,7 @@ class PseoMatrixEngine {
|
|
|
3787
3787
|
])
|
|
3788
3788
|
]
|
|
3789
3789
|
};
|
|
3790
|
-
|
|
3790
|
+
const pageItem = {
|
|
3791
3791
|
matrixFamily: "local-geo",
|
|
3792
3792
|
urlPath,
|
|
3793
3793
|
canonicalUrl: fullUrl,
|
|
@@ -3799,7 +3799,9 @@ class PseoMatrixEngine {
|
|
|
3799
3799
|
neighboringLinks: neighborLinks,
|
|
3800
3800
|
service: s,
|
|
3801
3801
|
location: loc
|
|
3802
|
-
}
|
|
3802
|
+
};
|
|
3803
|
+
pageItem.ogImageUrl = OgImageGenerator.generateOgImageUrl(cleanDomain, pageItem, options.brandName);
|
|
3804
|
+
pages.push(pageItem);
|
|
3803
3805
|
}
|
|
3804
3806
|
}
|
|
3805
3807
|
return pages;
|
|
@@ -4145,7 +4147,7 @@ class PseoMatrixEngine {
|
|
|
4145
4147
|
generateAllMatrices(domain, data, options) {
|
|
4146
4148
|
const allPages = [];
|
|
4147
4149
|
if (data.services) {
|
|
4148
|
-
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.
|
|
4150
|
+
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.allowedCountries || options.countries || options.territories || "us");
|
|
4149
4151
|
allPages.push(...this.generateLocalGeoMatrix(domain, data.services, locs, options));
|
|
4150
4152
|
}
|
|
4151
4153
|
if (data.competitors) {
|
|
@@ -4212,30 +4214,113 @@ class PseoMatrixEngine {
|
|
|
4212
4214
|
}
|
|
4213
4215
|
resolvePage(slugOrPath, domain, data, options) {
|
|
4214
4216
|
const path = typeof slugOrPath === "string" ? slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}` : `/${slugOrPath.join("/")}`;
|
|
4215
|
-
const
|
|
4216
|
-
const existing = pages.find((p) => p.urlPath === path);
|
|
4217
|
-
if (existing)
|
|
4218
|
-
return existing;
|
|
4217
|
+
const lang = options.language || "en";
|
|
4219
4218
|
const segments = path.split("/").filter(Boolean);
|
|
4220
|
-
if (segments.length
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4219
|
+
if (segments.length === 0)
|
|
4220
|
+
return;
|
|
4221
|
+
const vsPrefix = this.getPrefix("vs", lang).replace(/^\//, "");
|
|
4222
|
+
if (segments[0] === vsPrefix && segments[1] && data.competitors) {
|
|
4223
|
+
const compSlug = segments[1];
|
|
4224
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4225
|
+
if (matched) {
|
|
4226
|
+
return this.generateVsMatrix(domain, [matched], options)[0];
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
const altPrefix = this.getPrefix("alternative", lang).replace(/^\//, "");
|
|
4230
|
+
if (segments[0] === altPrefix && segments[1] && data.competitors) {
|
|
4231
|
+
const compSlug = segments[1];
|
|
4232
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4233
|
+
if (matched) {
|
|
4234
|
+
return this.generateAlternativesMatrix(domain, [matched], options)[0];
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
const pricingPrefix = this.getPrefix("pricing", lang).replace(/^\//, "");
|
|
4238
|
+
if (segments[0] === pricingPrefix && segments[1] && data.competitors) {
|
|
4239
|
+
const compSlug = segments[1];
|
|
4240
|
+
const matched = data.competitors.find((c) => cleanSeoSlug(c.slug, { language: lang }) === compSlug || c.slug === compSlug);
|
|
4241
|
+
if (matched) {
|
|
4242
|
+
return this.generatePricingMatrix(domain, [matched], options)[0];
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
const targetPrefix = this.getPrefix("target", lang).replace(/^\//, "");
|
|
4246
|
+
if (segments[0] === targetPrefix && segments[1] && data.targets) {
|
|
4247
|
+
const targetSlug = segments[1];
|
|
4248
|
+
const matched = data.targets.find((t) => cleanSeoSlug(t.slug, { language: lang }) === targetSlug || t.slug === targetSlug);
|
|
4249
|
+
if (matched) {
|
|
4250
|
+
return this.generateTargetMatrix(domain, [matched], options)[0];
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
const integPrefix = this.getPrefix("integration", lang).replace(/^\//, "");
|
|
4254
|
+
if (segments[0] === integPrefix && segments[1] && data.integrations) {
|
|
4255
|
+
const integSlug = segments[1];
|
|
4256
|
+
const matched = data.integrations.find((i) => cleanSeoSlug(i.slug, { language: lang }) === integSlug || i.slug === integSlug);
|
|
4257
|
+
if (matched) {
|
|
4258
|
+
return this.generateIntegrationMatrix(domain, [matched], options)[0];
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
const useCasePrefix = this.getPrefix("useCase", lang).replace(/^\//, "");
|
|
4262
|
+
if (segments[0] === useCasePrefix && segments[1] && data.useCases) {
|
|
4263
|
+
const ucSlug = segments[1];
|
|
4264
|
+
const matched = data.useCases.find((u) => cleanSeoSlug(u.slug, { language: lang }) === ucSlug || u.slug === ucSlug);
|
|
4265
|
+
if (matched) {
|
|
4266
|
+
return this.generateUseCaseMatrix(domain, [matched], options)[0];
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
const templatePrefix = this.getPrefix("template", lang).replace(/^\//, "");
|
|
4270
|
+
if (segments[0] === templatePrefix && segments[1] && data.templates) {
|
|
4271
|
+
const tSlug = segments[1];
|
|
4272
|
+
const matched = data.templates.find((t) => cleanSeoSlug(t.slug, { language: lang }) === tSlug || t.slug === tSlug);
|
|
4273
|
+
if (matched) {
|
|
4274
|
+
return this.generateTemplateMatrix(domain, [matched], options)[0];
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
const glossaryPrefix = this.getPrefix("glossary", lang).replace(/^\//, "");
|
|
4278
|
+
if (segments[0] === glossaryPrefix && segments[1] && data.glossaryTerms) {
|
|
4279
|
+
const gSlug = segments[1];
|
|
4280
|
+
const matched = data.glossaryTerms.find((g) => cleanSeoSlug(g.slug, { language: lang }) === gSlug || g.slug === gSlug);
|
|
4281
|
+
if (matched) {
|
|
4282
|
+
return this.generateGlossaryMatrix(domain, [matched], options)[0];
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
const calcPrefix = this.getPrefix("calculator", lang).replace(/^\//, "");
|
|
4286
|
+
if (segments[0] === calcPrefix && segments[1] && data.calculators) {
|
|
4287
|
+
const cSlug = segments[1];
|
|
4288
|
+
const matched = data.calculators.find((c) => cleanSeoSlug(c.slug, { language: lang }) === cSlug || c.slug === cSlug);
|
|
4289
|
+
if (matched) {
|
|
4290
|
+
return this.generateCalculatorMatrix(domain, [matched], options)[0];
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
if (data.services && data.services.length > 0) {
|
|
4294
|
+
let serviceSlug = "";
|
|
4295
|
+
let locationSlug = "";
|
|
4296
|
+
if (segments.length === 2 && options.cleanDirectRoutes !== false) {
|
|
4297
|
+
serviceSlug = segments[0];
|
|
4298
|
+
locationSlug = segments[1];
|
|
4299
|
+
} else if (segments.length >= 3 && segments[0] === this.getPrefix("solutions", lang).replace(/^\//, "")) {
|
|
4300
|
+
serviceSlug = segments[1];
|
|
4301
|
+
locationSlug = segments[segments.length - 1];
|
|
4302
|
+
}
|
|
4303
|
+
if (serviceSlug && locationSlug) {
|
|
4304
|
+
const matchedService = data.services.find((s) => cleanSeoSlug(s.slug, { language: lang }) === serviceSlug || s.slug === serviceSlug);
|
|
4305
|
+
if (matchedService) {
|
|
4306
|
+
let resolvedLoc = (data.locations || []).find((l) => cleanSeoSlug(l.slug, { language: lang }) === locationSlug || l.slug === locationSlug);
|
|
4307
|
+
if (!resolvedLoc) {
|
|
4308
|
+
const countryList = options.allowedCountries || options.countries;
|
|
4309
|
+
const fallbackCountry = Array.isArray(countryList) ? countryList[0] : typeof countryList === "string" && countryList !== "international" ? countryList : "United States";
|
|
4310
|
+
resolvedLoc = createDynamicLocation(locationSlug, fallbackCountry, {
|
|
4311
|
+
currency: options.defaultCurrency,
|
|
4312
|
+
currencySymbol: options.defaultCurrencySymbol
|
|
4313
|
+
});
|
|
4314
|
+
if (options.strictCountryBoundary && countryList) {
|
|
4315
|
+
const allowed = (Array.isArray(countryList) ? countryList : [countryList]).map((c) => c.toLowerCase());
|
|
4316
|
+
if (!allowed.includes("international") && !allowed.some((c) => resolvedLoc.country.toLowerCase().includes(c))) {
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4235
4320
|
}
|
|
4321
|
+
const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [resolvedLoc], options);
|
|
4322
|
+
return dynamicPages[0];
|
|
4236
4323
|
}
|
|
4237
|
-
const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [dynamicLoc], options);
|
|
4238
|
-
return dynamicPages.find((p) => p.urlPath === path) || dynamicPages[0];
|
|
4239
4324
|
}
|
|
4240
4325
|
}
|
|
4241
4326
|
return;
|
|
@@ -5045,7 +5130,20 @@ When discovering product modules, features, or services to build the SEO matrice
|
|
|
5045
5130
|
|
|
5046
5131
|
---
|
|
5047
5132
|
|
|
5048
|
-
### \
|
|
5133
|
+
### \uD83D\uDEA8 2. CRITICAL RULE: ZERO STATIC PRE-COMPILATION AT BUILD TIME (PURE RUNTIME ON-DEMAND)
|
|
5134
|
+
|
|
5135
|
+
- **❌ NEVER PRE-COMPILE CITIES OR PAGES AT BUILD TIME (\`generateStaticParams\` / \`getStaticPaths\`):**
|
|
5136
|
+
- DO NOT create static loops over 365, 61, or any hardcoded list of cities during \`next build\`.
|
|
5137
|
+
- DO NOT freeze pages into static HTML files on disk. That is NOT how high-scale programmatic SEO works.
|
|
5138
|
+
- Doing so breaks infinite dynamic expansion, causes build memory crashes, and falsely caps the website to a tiny subset of pages.
|
|
5139
|
+
- **✅ ALWAYS USE 100% RUNTIME IN-MEMORY RESOLUTION (\`resolvePage\`):**
|
|
5140
|
+
- In Next.js App Router: Always set \`export const dynamic = "force-dynamic"\` and \`export const dynamicParams = true\`.
|
|
5141
|
+
- When an incoming request arrives (e.g. \`/solutions/ac-repair/austin\`, \`/solutions/ac-repair/paris\`, \`/solutions/ac-repair/90210\`), the server executes \`matrixEngine.resolvePage([service, location], ...)\` in RAM in \`< 0.05ms\`.
|
|
5142
|
+
- Build time remains **under 3 seconds** because 0 localized landing pages are built ahead of time. Every page is synthesized on demand.
|
|
5143
|
+
|
|
5144
|
+
---
|
|
5145
|
+
|
|
5146
|
+
### \uD83C\uDF10 3. ARCHITECTURAL RULE: THE WEBSITE OWNS ITS LANGUAGES (i18n)
|
|
5049
5147
|
|
|
5050
5148
|
- **The Website (Application) is the sole owner of its languages, routes, and translations:**
|
|
5051
5149
|
- The website provides its own localized routes (e.g. \`app/[locale]/[...slug]/page.tsx\`) and dictionary files (\`next-intl\`, \`i18next\`, Astro i18n).
|
|
@@ -5308,7 +5406,7 @@ The SDK equips applications with 9 enterprise-grade SEO engines:
|
|
|
5308
5406
|
- Boosts dwell time, organic backlinks, and visitor-to-customer conversion.
|
|
5309
5407
|
|
|
5310
5408
|
8. **\uD83C\uDFDB️ Google Schema.org Rich Graphs (\`ExtendedSchemaGraphBuilder\`):**
|
|
5311
|
-
- \`buildAggregateRating\` (Google
|
|
5409
|
+
- \`buildAggregateRating\` (Verified live reviews synced from Google Places / Trustpilot / WooCommerce), \`buildAggregateOffer\` (price ranges), \`buildLocalBusiness\` (GPS & opening hours), \`buildSoftwareApplication\`, \`buildHowTo\`, \`buildFAQPage\`, \`buildBreadcrumbs\`.
|
|
5312
5410
|
|
|
5313
5411
|
9. **\uD83D\uDD0D URL Decomposition & Analysis (\`UrlyticsEngine\`):**
|
|
5314
5412
|
- Deep structural directory decomposition, depth tracking, query parameter analysis, and slug tokenization (inspired by advertools).
|
|
@@ -9565,6 +9663,245 @@ class PageRegenerationTracker {
|
|
|
9565
9663
|
}
|
|
9566
9664
|
}
|
|
9567
9665
|
|
|
9666
|
+
// src/google-indexing-client.ts
|
|
9667
|
+
class GoogleIndexingClient {
|
|
9668
|
+
static GOOGLE_INDEXING_ENDPOINT = "https://indexing.googleapis.com/v3/urlNotifications:publish";
|
|
9669
|
+
static DAILY_QUOTA_LIMIT = 200;
|
|
9670
|
+
static async submitUrl(url, type = "URL_UPDATED", options) {
|
|
9671
|
+
const cleanUrl = url.trim();
|
|
9672
|
+
if (!cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://")) {
|
|
9673
|
+
return {
|
|
9674
|
+
url: cleanUrl,
|
|
9675
|
+
type,
|
|
9676
|
+
status: "error",
|
|
9677
|
+
message: "Invalid URL format. Must start with http:// or https://"
|
|
9678
|
+
};
|
|
9679
|
+
}
|
|
9680
|
+
if (!options?.accessToken && !options?.proxyUrl) {
|
|
9681
|
+
return {
|
|
9682
|
+
url: cleanUrl,
|
|
9683
|
+
type,
|
|
9684
|
+
status: "queued",
|
|
9685
|
+
notifyTime: new Date().toISOString(),
|
|
9686
|
+
message: "URL queued in local batch queue. Provide Google Service Account token to broadcast live."
|
|
9687
|
+
};
|
|
9688
|
+
}
|
|
9689
|
+
try {
|
|
9690
|
+
const endpoint = options.proxyUrl || this.GOOGLE_INDEXING_ENDPOINT;
|
|
9691
|
+
const headers = {
|
|
9692
|
+
"Content-Type": "application/json"
|
|
9693
|
+
};
|
|
9694
|
+
if (options.accessToken) {
|
|
9695
|
+
headers["Authorization"] = `Bearer ${options.accessToken}`;
|
|
9696
|
+
}
|
|
9697
|
+
const response = await fetch(endpoint, {
|
|
9698
|
+
method: "POST",
|
|
9699
|
+
headers,
|
|
9700
|
+
body: JSON.stringify({ url: cleanUrl, type })
|
|
9701
|
+
});
|
|
9702
|
+
if (response.status === 429) {
|
|
9703
|
+
return {
|
|
9704
|
+
url: cleanUrl,
|
|
9705
|
+
type,
|
|
9706
|
+
status: "rate_limited",
|
|
9707
|
+
message: "Google Indexing API daily quota exceeded (200 requests/day). Queued for next window."
|
|
9708
|
+
};
|
|
9709
|
+
}
|
|
9710
|
+
if (!response.ok) {
|
|
9711
|
+
const errorText = await response.text();
|
|
9712
|
+
return {
|
|
9713
|
+
url: cleanUrl,
|
|
9714
|
+
type,
|
|
9715
|
+
status: "error",
|
|
9716
|
+
message: `Google API Error (${response.status}): ${errorText}`
|
|
9717
|
+
};
|
|
9718
|
+
}
|
|
9719
|
+
const data = await response.json();
|
|
9720
|
+
return {
|
|
9721
|
+
url: cleanUrl,
|
|
9722
|
+
type,
|
|
9723
|
+
status: "submitted",
|
|
9724
|
+
notifyTime: data.urlNotificationMetadata?.latestUpdate?.notifyTime || new Date().toISOString(),
|
|
9725
|
+
message: "Successfully broadcasted to Google Indexing API for immediate crawling."
|
|
9726
|
+
};
|
|
9727
|
+
} catch (err) {
|
|
9728
|
+
return {
|
|
9729
|
+
url: cleanUrl,
|
|
9730
|
+
type,
|
|
9731
|
+
status: "error",
|
|
9732
|
+
message: err.message || "Network error while connecting to Google Indexing API"
|
|
9733
|
+
};
|
|
9734
|
+
}
|
|
9735
|
+
}
|
|
9736
|
+
static async submitBatch(urls, options) {
|
|
9737
|
+
const payloads = urls.map((item) => typeof item === "string" ? { url: item, type: "URL_UPDATED" } : item);
|
|
9738
|
+
const maxBatch = options?.maxBatchSize || this.DAILY_QUOTA_LIMIT;
|
|
9739
|
+
const toProcess = payloads.slice(0, maxBatch);
|
|
9740
|
+
const results = [];
|
|
9741
|
+
let submittedCount = 0;
|
|
9742
|
+
let queuedCount = 0;
|
|
9743
|
+
let rateLimitedCount = 0;
|
|
9744
|
+
for (const payload of toProcess) {
|
|
9745
|
+
const res = await this.submitUrl(payload.url, payload.type, options);
|
|
9746
|
+
results.push(res);
|
|
9747
|
+
if (res.status === "submitted")
|
|
9748
|
+
submittedCount++;
|
|
9749
|
+
else if (res.status === "queued")
|
|
9750
|
+
queuedCount++;
|
|
9751
|
+
else if (res.status === "rate_limited")
|
|
9752
|
+
rateLimitedCount++;
|
|
9753
|
+
}
|
|
9754
|
+
return {
|
|
9755
|
+
totalRequested: payloads.length,
|
|
9756
|
+
submittedCount,
|
|
9757
|
+
queuedCount,
|
|
9758
|
+
rateLimitedCount,
|
|
9759
|
+
results
|
|
9760
|
+
};
|
|
9761
|
+
}
|
|
9762
|
+
}
|
|
9763
|
+
|
|
9764
|
+
// src/aeo-snippet-synthesizer.ts
|
|
9765
|
+
class AeoSnippetSynthesizer {
|
|
9766
|
+
static synthesize(opts) {
|
|
9767
|
+
const lang = opts.language || "en";
|
|
9768
|
+
const loc = opts.locationName ? `${opts.locationName}${opts.countryName ? `, ${opts.countryName}` : ""}` : "";
|
|
9769
|
+
const priceStr = opts.startingPrice ? `from ${opts.currencySymbol || "$"}${opts.startingPrice}/mo` : "with instant deployment";
|
|
9770
|
+
const benefits = opts.keyBenefits && opts.keyBenefits.length > 0 ? opts.keyBenefits.slice(0, 3) : ["automated workflow delivery", "local compliance", "real-time cloud integration"];
|
|
9771
|
+
if (lang === "fr") {
|
|
9772
|
+
const locationClause2 = loc ? ` à ${loc}` : "";
|
|
9773
|
+
const directAnswer2 = `${opts.brandName} propose des solutions de ${opts.serviceName}${locationClause2} ${priceStr}. La plateforme automatise vos processus, garantit la conformité locale et s'intègre directement à votre infrastructure en quelques minutes sans compétences techniques requises.`;
|
|
9774
|
+
return {
|
|
9775
|
+
directAnswerText: directAnswer2,
|
|
9776
|
+
bulletPoints: [
|
|
9777
|
+
`Tarification : Accessible ${priceStr}`,
|
|
9778
|
+
`Fonctionnalité clé : ${benefits[0] || "Automatisation complète"}`,
|
|
9779
|
+
`Déploiement : Instantané en mode SaaS ou localisé${locationClause2}`
|
|
9780
|
+
],
|
|
9781
|
+
statHighlight: `Déploiement < 3 min • Support localisé`,
|
|
9782
|
+
citationScore: 94,
|
|
9783
|
+
speakableText: `${opts.brandName} propose des solutions de ${opts.serviceName}${locationClause2}. Tarification accessible ${priceStr}.`
|
|
9784
|
+
};
|
|
9785
|
+
}
|
|
9786
|
+
if (lang === "es") {
|
|
9787
|
+
const locationClause2 = loc ? ` en ${loc}` : "";
|
|
9788
|
+
const directAnswer2 = `${opts.brandName} ofrece servicios profesionales de ${opts.serviceName}${locationClause2} ${priceStr}. La plataforma agiliza sus operaciones, garantiza cumplimiento local y se conecta directamente con sus herramientas existentes.`;
|
|
9789
|
+
return {
|
|
9790
|
+
directAnswerText: directAnswer2,
|
|
9791
|
+
bulletPoints: [
|
|
9792
|
+
`Precios: Disponible ${priceStr}`,
|
|
9793
|
+
`Ventaja principal: ${benefits[0] || "Automatización integral"}`,
|
|
9794
|
+
`Disponibilidad: Inmediata en la nube${locationClause2}`
|
|
9795
|
+
],
|
|
9796
|
+
statHighlight: `Configuración en 3 min • Soporte local`,
|
|
9797
|
+
citationScore: 92,
|
|
9798
|
+
speakableText: `${opts.brandName} ofrece servicios de ${opts.serviceName}${locationClause2} ${priceStr}.`
|
|
9799
|
+
};
|
|
9800
|
+
}
|
|
9801
|
+
if (lang === "de") {
|
|
9802
|
+
const locationClause2 = loc ? ` in ${loc}` : "";
|
|
9803
|
+
const directAnswer2 = `${opts.brandName} bietet professionelle ${opts.serviceName}-Lösungen${locationClause2} ${priceStr}. Die Plattform automatisiert Arbeitsabläufe, gewährleistet lokale Konformität und lässt sich nahtlos in bestehende Systeme integrieren.`;
|
|
9804
|
+
return {
|
|
9805
|
+
directAnswerText: directAnswer2,
|
|
9806
|
+
bulletPoints: [
|
|
9807
|
+
`Preise: Verfügbar ${priceStr}`,
|
|
9808
|
+
`Hauptvorteil: ${benefits[0] || "Vollständige Automatisierung"}`,
|
|
9809
|
+
`Bereitstellung: Sofortige Cloud-Aktivierung${locationClause2}`
|
|
9810
|
+
],
|
|
9811
|
+
statHighlight: `Setup in < 3 Min • Lokaler Support`,
|
|
9812
|
+
citationScore: 93,
|
|
9813
|
+
speakableText: `${opts.brandName} bietet ${opts.serviceName}${locationClause2} ${priceStr}.`
|
|
9814
|
+
};
|
|
9815
|
+
}
|
|
9816
|
+
const locationClause = loc ? ` in ${loc}` : "";
|
|
9817
|
+
const directAnswer = `${opts.brandName} provides enterprise-ready ${opts.serviceName} software${locationClause} ${priceStr}. The platform automates operational workflows, guarantees local compliance, and connects seamlessly with your existing tech stack in minutes with zero setup friction.`;
|
|
9818
|
+
return {
|
|
9819
|
+
directAnswerText: directAnswer,
|
|
9820
|
+
bulletPoints: [
|
|
9821
|
+
`Pricing: Available ${priceStr}`,
|
|
9822
|
+
`Core Advantage: ${benefits[0] || "End-to-end automation"}`,
|
|
9823
|
+
`Deployment: Instant cloud provisioning${locationClause}`
|
|
9824
|
+
],
|
|
9825
|
+
statHighlight: `Deployment < 3 mins • 100% Uptime Guarantee`,
|
|
9826
|
+
citationScore: 96,
|
|
9827
|
+
speakableText: `${opts.brandName} provides ${opts.serviceName}${locationClause} ${priceStr}.`
|
|
9828
|
+
};
|
|
9829
|
+
}
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
// src/semantic-cannibalization.ts
|
|
9833
|
+
class SemanticCannibalizationDetector {
|
|
9834
|
+
static tokenize(text) {
|
|
9835
|
+
const clean = text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
9836
|
+
return new Set(clean);
|
|
9837
|
+
}
|
|
9838
|
+
static jaccardSimilarity(setA, setB) {
|
|
9839
|
+
if (setA.size === 0 && setB.size === 0)
|
|
9840
|
+
return { score: 1, intersection: [] };
|
|
9841
|
+
if (setA.size === 0 || setB.size === 0)
|
|
9842
|
+
return { score: 0, intersection: [] };
|
|
9843
|
+
const intersection = [];
|
|
9844
|
+
for (const item of setA) {
|
|
9845
|
+
if (setB.has(item)) {
|
|
9846
|
+
intersection.push(item);
|
|
9847
|
+
}
|
|
9848
|
+
}
|
|
9849
|
+
const unionSize = setA.size + setB.size - intersection.length;
|
|
9850
|
+
const score = unionSize > 0 ? intersection.length / unionSize : 0;
|
|
9851
|
+
return { score: Math.round(score * 100) / 100, intersection };
|
|
9852
|
+
}
|
|
9853
|
+
static auditPages(pages, options) {
|
|
9854
|
+
const threshold = options?.similarityThreshold ?? 0.75;
|
|
9855
|
+
const maxAlerts = options?.maxAlerts ?? 100;
|
|
9856
|
+
const alerts = [];
|
|
9857
|
+
const tokenizedPages = pages.map((p) => ({
|
|
9858
|
+
page: p,
|
|
9859
|
+
tokens: this.tokenize(`${p.title} ${p.h1} ${(p.targetKeywords || []).join(" ")}`)
|
|
9860
|
+
}));
|
|
9861
|
+
for (let i = 0;i < tokenizedPages.length; i++) {
|
|
9862
|
+
for (let j = i + 1;j < tokenizedPages.length; j++) {
|
|
9863
|
+
if (alerts.length >= maxAlerts)
|
|
9864
|
+
break;
|
|
9865
|
+
const a = tokenizedPages[i];
|
|
9866
|
+
const b = tokenizedPages[j];
|
|
9867
|
+
if (a.page.urlPath === b.page.urlPath)
|
|
9868
|
+
continue;
|
|
9869
|
+
if (a.page.title.trim().toLowerCase() === b.page.title.trim().toLowerCase()) {
|
|
9870
|
+
alerts.push({
|
|
9871
|
+
primaryUrl: a.page.urlPath,
|
|
9872
|
+
conflictingUrl: b.page.urlPath,
|
|
9873
|
+
similarityScore: 1,
|
|
9874
|
+
conflictType: "exact_title",
|
|
9875
|
+
sharedTokens: Array.from(a.tokens),
|
|
9876
|
+
recommendation: `Differentiate Title tags. Add unique location modifier or service differentiator.`
|
|
9877
|
+
});
|
|
9878
|
+
continue;
|
|
9879
|
+
}
|
|
9880
|
+
const { score, intersection } = this.jaccardSimilarity(a.tokens, b.tokens);
|
|
9881
|
+
if (score >= threshold) {
|
|
9882
|
+
alerts.push({
|
|
9883
|
+
primaryUrl: a.page.urlPath,
|
|
9884
|
+
conflictingUrl: b.page.urlPath,
|
|
9885
|
+
similarityScore: score,
|
|
9886
|
+
conflictType: score > 0.85 ? "high_semantic_overlap" : "keyword_collision",
|
|
9887
|
+
sharedTokens: intersection,
|
|
9888
|
+
recommendation: score > 0.85 ? `High cannibalization risk (${Math.round(score * 100)}%). Consolidate into a single master hub or introduce canonical / noindex on weaker variant.` : `Differentiate intent between these two pages. Vary H1 subheadings and target distinct long-tail keywords.`
|
|
9889
|
+
});
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
const criticalCount = alerts.filter((a) => a.similarityScore >= 0.85).length;
|
|
9894
|
+
const moderateCount = alerts.length - criticalCount;
|
|
9895
|
+
return {
|
|
9896
|
+
totalPagesAudited: pages.length,
|
|
9897
|
+
totalAlerts: alerts.length,
|
|
9898
|
+
criticalCount,
|
|
9899
|
+
moderateCount,
|
|
9900
|
+
alerts
|
|
9901
|
+
};
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
|
|
9568
9905
|
// src/index.ts
|
|
9569
9906
|
function createLynxSeoEngine(config) {
|
|
9570
9907
|
return new LynxSeoEngine(config);
|
|
@@ -9576,6 +9913,9 @@ var LynxSeo = {
|
|
|
9576
9913
|
createLagoMeter: (apiKey, lagoUrl) => new LagoTokenMeter(apiKey, lagoUrl),
|
|
9577
9914
|
createHyperswitchGateway: (apiKey, baseUrl) => new HyperswitchGateway({ apiKey, baseUrl }),
|
|
9578
9915
|
submitToIndexNow: IndexNowClient.submitUrls,
|
|
9916
|
+
googleIndexing: GoogleIndexingClient,
|
|
9917
|
+
aeoSnippet: AeoSnippetSynthesizer,
|
|
9918
|
+
cannibalization: SemanticCannibalizationDetector,
|
|
9579
9919
|
inspectMeta: SiteAuditor.inspectMeta,
|
|
9580
9920
|
crawlDomain: DeepCrawlerAuditor.crawlAndAuditDomain,
|
|
9581
9921
|
inspectHtmlSnapshot: DeepCrawlerAuditor.inspectHtmlSnapshot,
|
|
@@ -9646,6 +9986,7 @@ export {
|
|
|
9646
9986
|
SerpRankHistoryEngine,
|
|
9647
9987
|
SerpClient,
|
|
9648
9988
|
SeoOpportunitiesDecayDetector,
|
|
9989
|
+
SemanticCannibalizationDetector,
|
|
9649
9990
|
SchemaGraphBuilder,
|
|
9650
9991
|
SUPPORTED_CANONICAL_LOCALES,
|
|
9651
9992
|
SCHEMA_LOCAL_BUSINESS_MAP,
|
|
@@ -9683,6 +10024,7 @@ export {
|
|
|
9683
10024
|
IndexNowClient,
|
|
9684
10025
|
I18nDetector,
|
|
9685
10026
|
HyperswitchGateway,
|
|
10027
|
+
GoogleIndexingClient,
|
|
9686
10028
|
GoogleBusinessProfileEngine,
|
|
9687
10029
|
GeoMeshLinkingEngine,
|
|
9688
10030
|
GeoCitationScorer,
|
|
@@ -9703,5 +10045,6 @@ export {
|
|
|
9703
10045
|
ApiKeyGuardian,
|
|
9704
10046
|
AiCopilotClient,
|
|
9705
10047
|
AiBotsLogAnalyzer,
|
|
10048
|
+
AeoSnippetSynthesizer,
|
|
9706
10049
|
AdIntelligenceCroEngine
|
|
9707
10050
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔍 Semantic Cannibalization & Keyword Overlap Detector
|
|
3
|
+
* Analyzes large programmatic page hubs (10,000+ pages) to detect semantic collisions,
|
|
4
|
+
* title duplicates, and keyword cannibalization between neighboring locations or services.
|
|
5
|
+
*/
|
|
6
|
+
export interface PageCrawlNode {
|
|
7
|
+
urlPath: string;
|
|
8
|
+
title: string;
|
|
9
|
+
h1: string;
|
|
10
|
+
targetKeywords?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface CannibalizationAlert {
|
|
13
|
+
primaryUrl: string;
|
|
14
|
+
conflictingUrl: string;
|
|
15
|
+
similarityScore: number;
|
|
16
|
+
conflictType: "exact_title" | "high_semantic_overlap" | "keyword_collision";
|
|
17
|
+
sharedTokens: string[];
|
|
18
|
+
recommendation: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CannibalizationAuditSummary {
|
|
21
|
+
totalPagesAudited: number;
|
|
22
|
+
totalAlerts: number;
|
|
23
|
+
criticalCount: number;
|
|
24
|
+
moderateCount: number;
|
|
25
|
+
alerts: CannibalizationAlert[];
|
|
26
|
+
}
|
|
27
|
+
export declare class SemanticCannibalizationDetector {
|
|
28
|
+
/**
|
|
29
|
+
* Tokenizes text and strips common punctuation for n-gram comparison.
|
|
30
|
+
*/
|
|
31
|
+
private static tokenize;
|
|
32
|
+
/**
|
|
33
|
+
* Computes Jaccard similarity between two token sets (0.0 to 1.0).
|
|
34
|
+
*/
|
|
35
|
+
private static jaccardSimilarity;
|
|
36
|
+
/**
|
|
37
|
+
* Audits a collection of pages and returns cannibalization risks.
|
|
38
|
+
*/
|
|
39
|
+
static auditPages(pages: PageCrawlNode[], options?: {
|
|
40
|
+
similarityThreshold?: number;
|
|
41
|
+
maxAlerts?: number;
|
|
42
|
+
}): CannibalizationAuditSummary;
|
|
43
|
+
}
|