@lynxflow/seo-engine 2.0.0 → 2.1.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 CHANGED
@@ -1,18 +1,19 @@
1
1
  # @lynxflow/seo-engine
2
2
 
3
- High-Performance Programmatic SEO & Structured Data Engine for Modern Web Applications.
3
+ High-Performance Universal Programmatic SEO & Structured Data Engine for Modern Web Applications.
4
4
 
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![License](https://img.shields.io/badge/License-Proprietary-green.svg)](LICENSE)
7
- [![Version](https://img.shields.io/badge/Version-2.0.0-orange.svg)](package.json)
7
+ [![Version](https://img.shields.io/badge/Version-2.1.0-orange.svg)](https://www.npmjs.com/package/@lynxflow/seo-engine)
8
8
 
9
9
  ---
10
10
 
11
11
  ## Overview
12
12
 
13
- `@lynxflow/seo-engine` is an enterprise developer SDK providing in-memory programmatic SEO matrix generation, Google-compliant Schema.org structured data, infinite geographic location synthesis, and real-time GeoIP Edge detection worldwide.
13
+ `@lynxflow/seo-engine` is an enterprise developer SDK providing in-memory programmatic SEO matrix generation, Google-compliant Schema.org structured data, infinite geographic location synthesis, strict market boundaries, and real-time GeoIP Edge detection worldwide.
14
14
 
15
15
  * **10 Canonical Root Pillars & 50+ Specialized Sub-Matrices:** Scales across industries, regulatory frameworks, role KPIs, multi-format templates, integration ecosystems, and ROI tools.
16
+ * **Strict Geographic & Linguistic Scoping:** Easily lock your website to a single country (e.g. 100% US or 100% France) or expand globally across multiple languages and territories with `strictCountryBoundary`.
16
17
  * **In-Memory Dynamic Execution:** Resolves millions of programmatic routes in RAM with zero database query overhead and zero MySQL/Postgres saturation.
17
18
  * **Infinite Geographic Resolution (`createDynamicLocation`):** Dynamically synthesizes any municipality, county, state, or postal code worldwide (e.g. `austin`, `scottsdale`, `75015`, `90210`, `ec1a-1bb`, `shinjuku`) on the fly without 404 errors.
18
19
  * **Edge GeoIP Location Detection (`resolveLocationFromHeaders`):** Reads Cloudflare (`cf-ipcity`, `cf-postal-code`), Vercel, AWS CloudFront, and Nginx edge headers to adapt pages to the visitor's real-time location.
@@ -34,33 +35,36 @@ bun add @lynxflow/seo-engine
34
35
 
35
36
  ---
36
37
 
37
- ## Core Matrix Architecture
38
+ ## Market Scoping: Single Country vs. Global Multi-Country
38
39
 
39
- The SDK structures programmatic content across **10 canonical root pillars** expanding into **50+ specialized sub-matrices**:
40
+ ### Option 1: 100% US-Only or Local Single-Country Website
41
+ Prevent crawl waste on out-of-scope foreign locations by enabling `strictCountryBoundary`:
40
42
 
43
+ ```typescript
44
+ export const SEO_CONFIG = {
45
+ brandName: "LoneStar HVAC Pro",
46
+ languages: ["en"],
47
+ countries: ["united-states"],
48
+ strictCountryBoundary: true, // 🛡️ Rejects non-US locations
49
+ defaultCurrency: "USD",
50
+ defaultCurrencySymbol: "$",
51
+ };
41
52
  ```
42
- ┌──────────────────────────────────────┬────────────────────────┬────────────────────────────────────────────────────────┐
43
- CANONICAL ROOT PILLAR │ SPECIALIZED DIMENSIONS │ HIGH-INTENT EXAMPLES │
44
- ├──────────────────────────────────────┼────────────────────────┼────────────────────────────────────────────────────────┤
45
- 👥 /for/{target} │ 12 Sub-Matrices │ Sector × Regulatory Compliance, Role × Daily KPIs │
46
- 🔌 /integrations/{app} │ 10 Sub-Matrices │ App Connector × Real-Time Webhook, Bi-Directional Sync │
47
- 📑 /templates/{slug} │ 8 Sub-Matrices │ Subject × Format (Excel, Notion, Google Sheets, PDF) │
48
- 🎯 /use-cases/{useCase} │ 8 Sub-Matrices │ Operational Pain Point × Step-by-Step Playbook │
49
- 🧮 /tools/{calculator} │ 6 Sub-Matrices │ Hours Saved Estimator, Revenue Uplift Simulator │
50
- │ 📚 /glossary/{term} │ 6 Sub-Matrices │ Financial Metrics (MRR/LTV), Technical Protocols (API) │
51
- │ 🥊 /vs/{competitor} │ Optional / Modular │ Architectural Comparison & Differences │
52
- │ 🔄 /alternatives/{competitor} │ Optional / Modular │ Clean Canonical Migration Guides │
53
- │ 💰 /pricing/{competitor} │ Optional / Modular │ Transparent Plan Breakdown & Verified Disclaimers │
54
- │ 📍 /solutions/{service}/{city} │ Local Geo Matrix │ Tiered Indexing (Crawl Budget) & Geodesic Mesh Links │
55
- └──────────────────────────────────────┴────────────────────────┴────────────────────────────────────────────────────────┘
53
+
54
+ ### Option 2: Worldwide Multi-Country / Multi-Lingual SaaS
55
+ ```typescript
56
+ export const SEO_CONFIG = {
57
+ brandName: "Apex Global Cloud",
58
+ languages: ["en", "de", "es", "fr", "ja", "pt-BR"],
59
+ countries: ["united-states", "united-kingdom", "germany", "france", "japan"],
60
+ strictCountryBoundary: false, // Allows worldwide expansion
61
+ };
56
62
  ```
57
63
 
58
64
  ---
59
65
 
60
66
  ## Quickstart & Contrasting Examples
61
67
 
62
- > **Important Note:** All examples below are **non-limiting illustrations**. You can pass any arbitrary language code and any arbitrary city or postal code worldwide.
63
-
64
68
  ### 1. Initialize Engine & Dataset
65
69
 
66
70
  ```typescript
@@ -68,14 +72,6 @@ import { PseoMatrixEngine } from "@lynxflow/seo-engine";
68
72
 
69
73
  export const matrixEngine = new PseoMatrixEngine();
70
74
 
71
- // Non-limiting example configuration (Supports ANY country or locale)
72
- export const SEO_CONFIG = {
73
- brandName: "Apex Global",
74
- languages: ["en", "de", "es", "fr", "ja", "pt-BR"], // Any ISO-639-1 / BCP-47 locale
75
- countries: ["united-states", "united-kingdom", "germany", "japan"], // Or "international" / any country
76
- cleanDirectRoutes: true,
77
- };
78
-
79
75
  export const PSEO_DATASET = {
80
76
  services: [
81
77
  {
@@ -93,10 +89,6 @@ export const PSEO_DATASET = {
93
89
  description: "Continuous enterprise backup and disaster recovery"
94
90
  },
95
91
  ],
96
- targets: [
97
- { slug: "enterprises", name: "Enterprise Organizations", type: "b2b", painPoints: ["Downtime Risk"], benefits: ["99.99% SLA"] },
98
- { slug: "healthcare", name: "Healthcare Providers", type: "industry", painPoints: ["HIPAA Compliance"], benefits: ["Audit-Ready Storage"] }
99
- ],
100
92
  };
101
93
  ```
102
94
 
@@ -104,11 +96,10 @@ export const PSEO_DATASET = {
104
96
 
105
97
  ```typescript
106
98
  // Case 1: Standard Pre-Indexed City in English
107
- const pageStandard = matrixEngine.resolvePage(["commercial-hvac", "london"], "https://example.com", PSEO_DATASET, SEO_CONFIG);
99
+ const pageStandard = matrixEngine.resolvePage(["commercial-hvac", "new-york"], "https://example.com", PSEO_DATASET, SEO_CONFIG);
108
100
 
109
- // Case 2: Contrasting Case — Arbitrary Postal Code / Custom District in Japanese
101
+ // Case 2: Contrasting Case — Arbitrary Postal Code / Custom District
110
102
  const pageCustom = matrixEngine.resolvePage(["commercial-hvac", "90210"], "https://example.com", PSEO_DATASET, SEO_CONFIG);
111
- const pageJapanese = matrixEngine.resolvePage(["ja", "commercial-hvac", "shinjuku"], "https://example.com", PSEO_DATASET, SEO_CONFIG);
112
103
  ```
113
104
 
114
105
  ### 3. Edge GeoIP Resolution Example
@@ -118,7 +109,6 @@ import { resolveLocationFromHeaders, PseoMatrixEngine } from "@lynxflow/seo-engi
118
109
 
119
110
  export function handleIncomingRequest(request: Request) {
120
111
  const geo = resolveLocationFromHeaders(request.headers);
121
- // Extracted dynamically from Cloudflare / Vercel / CloudFront headers
122
112
  const userCity = geo.city || "new-york";
123
113
 
124
114
  return matrixEngine.resolvePage(["commercial-hvac", userCity], "https://example.com", PSEO_DATASET, SEO_CONFIG);
@@ -129,11 +119,10 @@ export function handleIncomingRequest(request: Request) {
129
119
 
130
120
  ## TypeScript API Reference
131
121
 
132
- * **`PseoMatrixEngine`**: Core programmatic matrix compiler across all 10 root pillars and 50+ sub-matrices.
133
- * **`createDynamicLocation(slugOrInput, fallbackCountry)`**: Dynamic synthesis of any city, postal code or district worldwide at runtime.
122
+ * **`PseoMatrixEngine`**: Core programmatic matrix compiler with geographic boundaries and crawl budget protection.
123
+ * **`createDynamicLocation(slugOrInput, fallbackCountry, options)`**: Dynamic synthesis of any city, postal code or district worldwide.
134
124
  * **`resolveLocationFromHeaders(headers)`**: Edge header GeoIP extractor for Cloudflare, Vercel, CloudFront, and Nginx.
135
125
  * **`cleanSeoSlug(text, options)`**: Multilingual slug normalizer with stop-word removal for 10+ languages.
136
- * **`validateSeoSlug(slug)`**: Strict slug validator.
137
126
  * **`ExtendedSchemaGraphBuilder`**: 50+ Google Rich Result Schema.org JSON-LD generator.
138
127
  * **`LegalDisclaimerEngine`**: Verified compliance notice manager with staleness guards (>90 days).
139
128
  * **`getSeoAgentPrompt(context)`**: Built-in LLM operational prompt for AI coding agents.
@@ -15,7 +15,11 @@ export declare function resolveBuiltInLocations(territories?: string[] | string)
15
15
  * Allows resolving ANY unlisted city, commune, district, or postal code dynamically on demand
16
16
  * without returning 404 or requiring hardcoded databases.
17
17
  */
18
- export declare function createDynamicLocation(slugOrInput: string, fallbackCountry?: string): PseoLocation;
18
+ export declare function createDynamicLocation(slugOrInput: string, fallbackCountry?: string, options?: {
19
+ currency?: string;
20
+ currencySymbol?: string;
21
+ region?: string;
22
+ }): PseoLocation;
19
23
  /**
20
24
  * 📍 GeoIP Edge Headers Resolver (Dynamic User Geolocation)
21
25
  * Extracts the user's real-time city and country from Cloudflare / Vercel / Nginx Edge headers.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 📊 Page Generation & Multi-Cycle Regeneration Tracker
3
+ *
4
+ * Tracks:
5
+ * 1. Unique Live Programmatic Pages in Index
6
+ * 2. Total Generation Cycles (Initial Creation + 2x, 3x+ Re-Optimizations)
7
+ * 3. Execution Mode: 'standard' (Deterministic 0€, 0 Token) vs 'ai-enhanced' (LLM Custom Copy with Token Billing)
8
+ * 4. Market Demand Signals & Emerging Country Opportunities (e.g. Brazil, US, Spain)
9
+ */
10
+ export interface PageGenerationRecord {
11
+ urlPath: string;
12
+ service: string;
13
+ location: string;
14
+ country: string;
15
+ language: string;
16
+ generationCount: number;
17
+ mode: "standard" | "ai-enhanced";
18
+ tokensConsumed: number;
19
+ firstGeneratedAt: string;
20
+ lastGeneratedAt: string;
21
+ }
22
+ export interface MarketOpportunitySignal {
23
+ country: string;
24
+ countryCode: string;
25
+ detectedRequests: number;
26
+ activeIndexedPages: number;
27
+ coverageStatus: "optimal" | "underserved" | "opportunity_detected";
28
+ estimatedMonthlyTrafficUpside: number;
29
+ recommendation: string;
30
+ }
31
+ export interface GenerationMetricsReport {
32
+ uniquePagesCount: number;
33
+ totalGenerationCycles: number;
34
+ regenerationFrequency: {
35
+ singleGeneration: number;
36
+ doubleGeneration: number;
37
+ multiGeneration: number;
38
+ };
39
+ modeDistribution: {
40
+ standardDeterministicCount: number;
41
+ aiEnhancedCount: number;
42
+ };
43
+ totalAiTokensConsumed: number;
44
+ activeCountriesCount: number;
45
+ marketOpportunities: MarketOpportunitySignal[];
46
+ }
47
+ export declare class PageRegenerationTracker {
48
+ private records;
49
+ private demandSignals;
50
+ /**
51
+ * Records a page generation or regeneration event.
52
+ */
53
+ recordGeneration(params: {
54
+ urlPath: string;
55
+ service?: string;
56
+ location?: string;
57
+ country?: string;
58
+ language?: string;
59
+ mode?: "standard" | "ai-enhanced";
60
+ tokensConsumed?: number;
61
+ }): PageGenerationRecord;
62
+ /**
63
+ * Ingests external traffic or search demand signals (e.g. from Cloudflare / Nginx Edge / Google Search Console).
64
+ */
65
+ recordMarketDemand(country: string, requestVolume?: number): void;
66
+ /**
67
+ * Computes comprehensive analytics report for admin dashboards.
68
+ */
69
+ getMetricsReport(): GenerationMetricsReport;
70
+ }
package/dist/index.d.ts CHANGED
@@ -110,6 +110,7 @@ import { LynxRateLimitedTranslator } from "./rate-limited-translator";
110
110
  import { InternalPageRankEngine } from "./internal-pagerank-graph";
111
111
  import { GeoCitationScorer } from "./geo-citation-scorer";
112
112
  import { MarketingSkillsEngine } from "./marketing-skills-engine";
113
+ import { PageRegenerationTracker } from "./generation-tracker";
113
114
  export declare function createLynxSeoEngine(config: EngineConfig): LynxSeoEngine;
114
115
  export declare const LynxSeo: {
115
116
  createEngine: typeof createLynxSeoEngine;
@@ -157,7 +158,9 @@ export declare const LynxSeo: {
157
158
  pageRankGraph: typeof InternalPageRankEngine;
158
159
  geoCitation: typeof GeoCitationScorer;
159
160
  marketingSkills: typeof MarketingSkillsEngine;
161
+ regenerationTracker: typeof PageRegenerationTracker;
160
162
  };
163
+ export * from "./generation-tracker";
161
164
  export * from "./internal-pagerank-graph";
162
165
  export * from "./geo-citation-scorer";
163
166
  export * from "./marketing-skills-engine";
package/dist/index.js CHANGED
@@ -3524,26 +3524,43 @@ function resolveBuiltInLocations(territories) {
3524
3524
  }
3525
3525
  }
3526
3526
  }
3527
- return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
3527
+ return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.us];
3528
3528
  }
3529
- function createDynamicLocation(slugOrInput, fallbackCountry = "France") {
3529
+ function createDynamicLocation(slugOrInput, fallbackCountry = "United States", options) {
3530
3530
  const cleanInput = slugOrInput.trim().toLowerCase();
3531
3531
  const slug = cleanInput.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3532
3532
  const formattedName = slug.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3533
- const isPostalCode = /^\d{4,5}$/.test(cleanInput);
3534
- const displayName = isPostalCode ? `Secteur ${cleanInput}` : formattedName;
3535
- const isUS = fallbackCountry.toLowerCase().includes("us") || fallbackCountry.toLowerCase().includes("state");
3536
- const isUK = fallbackCountry.toLowerCase().includes("uk") || fallbackCountry.toLowerCase().includes("kingdom");
3533
+ const isUsZip = /^\d{5}(-\d{4})?$/.test(cleanInput);
3534
+ const isFrenchPostal = /^\d{5}$/.test(cleanInput) && (fallbackCountry.toLowerCase().includes("fr") || fallbackCountry.toLowerCase().includes("france"));
3535
+ let displayName = formattedName;
3536
+ let regionName = options?.region || "Metropolitan Area";
3537
+ if (isUsZip) {
3538
+ displayName = `Area ${cleanInput}`;
3539
+ regionName = `ZIP Code ${cleanInput}`;
3540
+ } else if (isFrenchPostal) {
3541
+ displayName = `Secteur ${cleanInput}`;
3542
+ regionName = `Département ${cleanInput.slice(0, 2)}`;
3543
+ }
3544
+ const countryLower = fallbackCountry.toLowerCase();
3545
+ const isUS = countryLower.includes("us") || countryLower.includes("state") || countryLower.includes("america");
3546
+ const isUK = countryLower.includes("uk") || countryLower.includes("kingdom") || countryLower.includes("britain");
3547
+ const isJapan = countryLower.includes("japan") || countryLower.includes("jp") || countryLower.includes("tokyo");
3548
+ const isCanada = countryLower.includes("canada") || countryLower.includes("ca");
3549
+ const isAustralia = countryLower.includes("australia") || countryLower.includes("au");
3550
+ const isBrazil = countryLower.includes("brazil") || countryLower.includes("br") || countryLower.includes("brasil");
3551
+ const isSwitzerland = countryLower.includes("switzerland") || countryLower.includes("swiss") || countryLower.includes("ch");
3552
+ const currency = options?.currency || (isUS ? "USD" : isUK ? "GBP" : isJapan ? "JPY" : isCanada ? "CAD" : isAustralia ? "AUD" : isBrazil ? "BRL" : isSwitzerland ? "CHF" : "EUR");
3553
+ const currencySymbol = options?.currencySymbol || (isUS ? "$" : isUK ? "£" : isJapan ? "¥" : isCanada ? "CAD $" : isAustralia ? "AUD $" : isBrazil ? "R$" : isSwitzerland ? "CHF" : "€");
3537
3554
  return {
3538
3555
  slug,
3539
3556
  name: displayName,
3540
- region: isPostalCode ? `Département ${cleanInput.slice(0, 2)}` : "Région Métropolitaine",
3557
+ region: regionName,
3541
3558
  country: fallbackCountry,
3542
- currency: isUS ? "USD" : isUK ? "GBP" : "EUR",
3543
- currencySymbol: isUS ? "$" : isUK ? "£" : "€",
3544
- population: 45000,
3545
- latitude: 48.8566,
3546
- longitude: 2.3522,
3559
+ currency,
3560
+ currencySymbol,
3561
+ population: 50000,
3562
+ latitude: 40.7128,
3563
+ longitude: -74.006,
3547
3564
  neighborSlugs: []
3548
3565
  };
3549
3566
  }
@@ -4205,7 +4222,18 @@ class PseoMatrixEngine {
4205
4222
  const locationSlug = segments[segments.length - 1];
4206
4223
  const matchedService = data.services.find((s) => cleanSeoSlug(s.slug, { language: options.language }) === serviceSlug || s.slug === serviceSlug);
4207
4224
  if (matchedService) {
4208
- const dynamicLoc = createDynamicLocation(locationSlug, options.countries?.[0] || "France");
4225
+ const countryList = options.allowedCountries || options.countries;
4226
+ const fallbackCountry = Array.isArray(countryList) ? countryList[0] : typeof countryList === "string" && countryList !== "international" ? countryList : "United States";
4227
+ const dynamicLoc = createDynamicLocation(locationSlug, fallbackCountry, {
4228
+ currency: options.defaultCurrency,
4229
+ currencySymbol: options.defaultCurrencySymbol
4230
+ });
4231
+ if (options.strictCountryBoundary && countryList) {
4232
+ const allowed = (Array.isArray(countryList) ? countryList : [countryList]).map((c) => c.toLowerCase());
4233
+ if (!allowed.includes("international") && !allowed.some((c) => dynamicLoc.country.toLowerCase().includes(c))) {
4234
+ return;
4235
+ }
4236
+ }
4209
4237
  const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [dynamicLoc], options);
4210
4238
  return dynamicPages.find((p) => p.urlPath === path) || dynamicPages[0];
4211
4239
  }
@@ -9429,6 +9457,114 @@ class MarketingSkillsEngine {
9429
9457
  }
9430
9458
  }
9431
9459
 
9460
+ // src/generation-tracker.ts
9461
+ class PageRegenerationTracker {
9462
+ records = new Map;
9463
+ demandSignals = new Map;
9464
+ recordGeneration(params) {
9465
+ const key = params.urlPath.trim().toLowerCase();
9466
+ const existing = this.records.get(key);
9467
+ const now = new Date().toISOString();
9468
+ const country = params.country || existing?.country || "United States";
9469
+ const language = params.language || existing?.language || "en";
9470
+ const service = params.service || existing?.service || "General Service";
9471
+ const location = params.location || existing?.location || "National";
9472
+ const mode = params.mode || "standard";
9473
+ const tokens = params.tokensConsumed || 0;
9474
+ if (existing) {
9475
+ existing.generationCount += 1;
9476
+ existing.mode = mode;
9477
+ existing.tokensConsumed += tokens;
9478
+ existing.lastGeneratedAt = now;
9479
+ this.records.set(key, existing);
9480
+ return existing;
9481
+ }
9482
+ const newRecord = {
9483
+ urlPath: key,
9484
+ service,
9485
+ location,
9486
+ country,
9487
+ language,
9488
+ generationCount: 1,
9489
+ mode,
9490
+ tokensConsumed: tokens,
9491
+ firstGeneratedAt: now,
9492
+ lastGeneratedAt: now
9493
+ };
9494
+ this.records.set(key, newRecord);
9495
+ const countryKey = country.toLowerCase();
9496
+ const currentSignal = this.demandSignals.get(countryKey) || { requests: 0, country };
9497
+ currentSignal.requests += 1;
9498
+ this.demandSignals.set(countryKey, currentSignal);
9499
+ return newRecord;
9500
+ }
9501
+ recordMarketDemand(country, requestVolume = 1) {
9502
+ const countryKey = country.trim().toLowerCase();
9503
+ const current = this.demandSignals.get(countryKey) || { requests: 0, country };
9504
+ current.requests += requestVolume;
9505
+ this.demandSignals.set(countryKey, current);
9506
+ }
9507
+ getMetricsReport() {
9508
+ let singleGen = 0;
9509
+ let doubleGen = 0;
9510
+ let multiGen = 0;
9511
+ let standardCount = 0;
9512
+ let aiCount = 0;
9513
+ let totalTokens = 0;
9514
+ const countriesSet = new Set;
9515
+ const countryPageCounts = new Map;
9516
+ for (const record of this.records.values()) {
9517
+ if (record.generationCount === 1)
9518
+ singleGen++;
9519
+ else if (record.generationCount === 2)
9520
+ doubleGen++;
9521
+ else
9522
+ multiGen++;
9523
+ if (record.mode === "ai-enhanced") {
9524
+ aiCount++;
9525
+ totalTokens += record.tokensConsumed;
9526
+ } else {
9527
+ standardCount++;
9528
+ }
9529
+ countriesSet.add(record.country);
9530
+ const cKey = record.country.toLowerCase();
9531
+ countryPageCounts.set(cKey, (countryPageCounts.get(cKey) || 0) + 1);
9532
+ }
9533
+ const marketOpportunities = [];
9534
+ for (const [countryKey, signal] of this.demandSignals.entries()) {
9535
+ const indexedPages = countryPageCounts.get(countryKey) || 0;
9536
+ const countryFormatted = signal.country.charAt(0).toUpperCase() + signal.country.slice(1);
9537
+ if (signal.requests > 100 && indexedPages < 20) {
9538
+ marketOpportunities.push({
9539
+ country: countryFormatted,
9540
+ countryCode: countryKey.slice(0, 2).toUpperCase(),
9541
+ detectedRequests: signal.requests,
9542
+ activeIndexedPages: indexedPages,
9543
+ coverageStatus: "opportunity_detected",
9544
+ estimatedMonthlyTrafficUpside: Math.round(signal.requests * 3.4),
9545
+ recommendation: `High demand detected in ${countryFormatted} (${signal.requests} hits). Upgrade to Pro Growth or activate ${countryFormatted} in SEO_CONFIG to capture +${Math.round(signal.requests * 3.4)} visits/month.`
9546
+ });
9547
+ }
9548
+ }
9549
+ return {
9550
+ uniquePagesCount: this.records.size,
9551
+ totalGenerationCycles: singleGen + doubleGen * 2 + multiGen * 3,
9552
+ regenerationFrequency: {
9553
+ singleGeneration: singleGen,
9554
+ doubleGeneration: doubleGen,
9555
+ multiGeneration: multiGen
9556
+ },
9557
+ modeDistribution: {
9558
+ standardDeterministicCount: standardCount,
9559
+ aiEnhancedCount: aiCount
9560
+ },
9561
+ totalAiTokensConsumed: totalTokens,
9562
+ activeCountriesCount: countriesSet.size,
9563
+ marketOpportunities
9564
+ };
9565
+ }
9566
+ }
9567
+
9432
9568
  // src/index.ts
9433
9569
  function createLynxSeoEngine(config) {
9434
9570
  return new LynxSeoEngine(config);
@@ -9478,7 +9614,8 @@ var LynxSeo = {
9478
9614
  translator: LynxRateLimitedTranslator,
9479
9615
  pageRankGraph: InternalPageRankEngine,
9480
9616
  geoCitation: GeoCitationScorer,
9481
- marketingSkills: MarketingSkillsEngine
9617
+ marketingSkills: MarketingSkillsEngine,
9618
+ regenerationTracker: PageRegenerationTracker
9482
9619
  };
9483
9620
  var src_default = LynxSeo;
9484
9621
  export {
@@ -9518,6 +9655,7 @@ export {
9518
9655
  PublicRoutesManifestEngine,
9519
9656
  PseoMatrixEngine,
9520
9657
  PowerWordsPsychologyEngine,
9658
+ PageRegenerationTracker,
9521
9659
  PSEO_AGENT_SYSTEM_PROMPT,
9522
9660
  OgImageGenerator,
9523
9661
  NGramDensityAnalyzer,
package/dist/index.mjs CHANGED
@@ -3524,26 +3524,43 @@ function resolveBuiltInLocations(territories) {
3524
3524
  }
3525
3525
  }
3526
3526
  }
3527
- return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
3527
+ return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.us];
3528
3528
  }
3529
- function createDynamicLocation(slugOrInput, fallbackCountry = "France") {
3529
+ function createDynamicLocation(slugOrInput, fallbackCountry = "United States", options) {
3530
3530
  const cleanInput = slugOrInput.trim().toLowerCase();
3531
3531
  const slug = cleanInput.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3532
3532
  const formattedName = slug.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3533
- const isPostalCode = /^\d{4,5}$/.test(cleanInput);
3534
- const displayName = isPostalCode ? `Secteur ${cleanInput}` : formattedName;
3535
- const isUS = fallbackCountry.toLowerCase().includes("us") || fallbackCountry.toLowerCase().includes("state");
3536
- const isUK = fallbackCountry.toLowerCase().includes("uk") || fallbackCountry.toLowerCase().includes("kingdom");
3533
+ const isUsZip = /^\d{5}(-\d{4})?$/.test(cleanInput);
3534
+ const isFrenchPostal = /^\d{5}$/.test(cleanInput) && (fallbackCountry.toLowerCase().includes("fr") || fallbackCountry.toLowerCase().includes("france"));
3535
+ let displayName = formattedName;
3536
+ let regionName = options?.region || "Metropolitan Area";
3537
+ if (isUsZip) {
3538
+ displayName = `Area ${cleanInput}`;
3539
+ regionName = `ZIP Code ${cleanInput}`;
3540
+ } else if (isFrenchPostal) {
3541
+ displayName = `Secteur ${cleanInput}`;
3542
+ regionName = `Département ${cleanInput.slice(0, 2)}`;
3543
+ }
3544
+ const countryLower = fallbackCountry.toLowerCase();
3545
+ const isUS = countryLower.includes("us") || countryLower.includes("state") || countryLower.includes("america");
3546
+ const isUK = countryLower.includes("uk") || countryLower.includes("kingdom") || countryLower.includes("britain");
3547
+ const isJapan = countryLower.includes("japan") || countryLower.includes("jp") || countryLower.includes("tokyo");
3548
+ const isCanada = countryLower.includes("canada") || countryLower.includes("ca");
3549
+ const isAustralia = countryLower.includes("australia") || countryLower.includes("au");
3550
+ const isBrazil = countryLower.includes("brazil") || countryLower.includes("br") || countryLower.includes("brasil");
3551
+ const isSwitzerland = countryLower.includes("switzerland") || countryLower.includes("swiss") || countryLower.includes("ch");
3552
+ const currency = options?.currency || (isUS ? "USD" : isUK ? "GBP" : isJapan ? "JPY" : isCanada ? "CAD" : isAustralia ? "AUD" : isBrazil ? "BRL" : isSwitzerland ? "CHF" : "EUR");
3553
+ const currencySymbol = options?.currencySymbol || (isUS ? "$" : isUK ? "£" : isJapan ? "¥" : isCanada ? "CAD $" : isAustralia ? "AUD $" : isBrazil ? "R$" : isSwitzerland ? "CHF" : "€");
3537
3554
  return {
3538
3555
  slug,
3539
3556
  name: displayName,
3540
- region: isPostalCode ? `Département ${cleanInput.slice(0, 2)}` : "Région Métropolitaine",
3557
+ region: regionName,
3541
3558
  country: fallbackCountry,
3542
- currency: isUS ? "USD" : isUK ? "GBP" : "EUR",
3543
- currencySymbol: isUS ? "$" : isUK ? "£" : "€",
3544
- population: 45000,
3545
- latitude: 48.8566,
3546
- longitude: 2.3522,
3559
+ currency,
3560
+ currencySymbol,
3561
+ population: 50000,
3562
+ latitude: 40.7128,
3563
+ longitude: -74.006,
3547
3564
  neighborSlugs: []
3548
3565
  };
3549
3566
  }
@@ -4205,7 +4222,18 @@ class PseoMatrixEngine {
4205
4222
  const locationSlug = segments[segments.length - 1];
4206
4223
  const matchedService = data.services.find((s) => cleanSeoSlug(s.slug, { language: options.language }) === serviceSlug || s.slug === serviceSlug);
4207
4224
  if (matchedService) {
4208
- const dynamicLoc = createDynamicLocation(locationSlug, options.countries?.[0] || "France");
4225
+ const countryList = options.allowedCountries || options.countries;
4226
+ const fallbackCountry = Array.isArray(countryList) ? countryList[0] : typeof countryList === "string" && countryList !== "international" ? countryList : "United States";
4227
+ const dynamicLoc = createDynamicLocation(locationSlug, fallbackCountry, {
4228
+ currency: options.defaultCurrency,
4229
+ currencySymbol: options.defaultCurrencySymbol
4230
+ });
4231
+ if (options.strictCountryBoundary && countryList) {
4232
+ const allowed = (Array.isArray(countryList) ? countryList : [countryList]).map((c) => c.toLowerCase());
4233
+ if (!allowed.includes("international") && !allowed.some((c) => dynamicLoc.country.toLowerCase().includes(c))) {
4234
+ return;
4235
+ }
4236
+ }
4209
4237
  const dynamicPages = this.generateLocalGeoMatrix(domain, [matchedService], [dynamicLoc], options);
4210
4238
  return dynamicPages.find((p) => p.urlPath === path) || dynamicPages[0];
4211
4239
  }
@@ -9429,6 +9457,114 @@ class MarketingSkillsEngine {
9429
9457
  }
9430
9458
  }
9431
9459
 
9460
+ // src/generation-tracker.ts
9461
+ class PageRegenerationTracker {
9462
+ records = new Map;
9463
+ demandSignals = new Map;
9464
+ recordGeneration(params) {
9465
+ const key = params.urlPath.trim().toLowerCase();
9466
+ const existing = this.records.get(key);
9467
+ const now = new Date().toISOString();
9468
+ const country = params.country || existing?.country || "United States";
9469
+ const language = params.language || existing?.language || "en";
9470
+ const service = params.service || existing?.service || "General Service";
9471
+ const location = params.location || existing?.location || "National";
9472
+ const mode = params.mode || "standard";
9473
+ const tokens = params.tokensConsumed || 0;
9474
+ if (existing) {
9475
+ existing.generationCount += 1;
9476
+ existing.mode = mode;
9477
+ existing.tokensConsumed += tokens;
9478
+ existing.lastGeneratedAt = now;
9479
+ this.records.set(key, existing);
9480
+ return existing;
9481
+ }
9482
+ const newRecord = {
9483
+ urlPath: key,
9484
+ service,
9485
+ location,
9486
+ country,
9487
+ language,
9488
+ generationCount: 1,
9489
+ mode,
9490
+ tokensConsumed: tokens,
9491
+ firstGeneratedAt: now,
9492
+ lastGeneratedAt: now
9493
+ };
9494
+ this.records.set(key, newRecord);
9495
+ const countryKey = country.toLowerCase();
9496
+ const currentSignal = this.demandSignals.get(countryKey) || { requests: 0, country };
9497
+ currentSignal.requests += 1;
9498
+ this.demandSignals.set(countryKey, currentSignal);
9499
+ return newRecord;
9500
+ }
9501
+ recordMarketDemand(country, requestVolume = 1) {
9502
+ const countryKey = country.trim().toLowerCase();
9503
+ const current = this.demandSignals.get(countryKey) || { requests: 0, country };
9504
+ current.requests += requestVolume;
9505
+ this.demandSignals.set(countryKey, current);
9506
+ }
9507
+ getMetricsReport() {
9508
+ let singleGen = 0;
9509
+ let doubleGen = 0;
9510
+ let multiGen = 0;
9511
+ let standardCount = 0;
9512
+ let aiCount = 0;
9513
+ let totalTokens = 0;
9514
+ const countriesSet = new Set;
9515
+ const countryPageCounts = new Map;
9516
+ for (const record of this.records.values()) {
9517
+ if (record.generationCount === 1)
9518
+ singleGen++;
9519
+ else if (record.generationCount === 2)
9520
+ doubleGen++;
9521
+ else
9522
+ multiGen++;
9523
+ if (record.mode === "ai-enhanced") {
9524
+ aiCount++;
9525
+ totalTokens += record.tokensConsumed;
9526
+ } else {
9527
+ standardCount++;
9528
+ }
9529
+ countriesSet.add(record.country);
9530
+ const cKey = record.country.toLowerCase();
9531
+ countryPageCounts.set(cKey, (countryPageCounts.get(cKey) || 0) + 1);
9532
+ }
9533
+ const marketOpportunities = [];
9534
+ for (const [countryKey, signal] of this.demandSignals.entries()) {
9535
+ const indexedPages = countryPageCounts.get(countryKey) || 0;
9536
+ const countryFormatted = signal.country.charAt(0).toUpperCase() + signal.country.slice(1);
9537
+ if (signal.requests > 100 && indexedPages < 20) {
9538
+ marketOpportunities.push({
9539
+ country: countryFormatted,
9540
+ countryCode: countryKey.slice(0, 2).toUpperCase(),
9541
+ detectedRequests: signal.requests,
9542
+ activeIndexedPages: indexedPages,
9543
+ coverageStatus: "opportunity_detected",
9544
+ estimatedMonthlyTrafficUpside: Math.round(signal.requests * 3.4),
9545
+ recommendation: `High demand detected in ${countryFormatted} (${signal.requests} hits). Upgrade to Pro Growth or activate ${countryFormatted} in SEO_CONFIG to capture +${Math.round(signal.requests * 3.4)} visits/month.`
9546
+ });
9547
+ }
9548
+ }
9549
+ return {
9550
+ uniquePagesCount: this.records.size,
9551
+ totalGenerationCycles: singleGen + doubleGen * 2 + multiGen * 3,
9552
+ regenerationFrequency: {
9553
+ singleGeneration: singleGen,
9554
+ doubleGeneration: doubleGen,
9555
+ multiGeneration: multiGen
9556
+ },
9557
+ modeDistribution: {
9558
+ standardDeterministicCount: standardCount,
9559
+ aiEnhancedCount: aiCount
9560
+ },
9561
+ totalAiTokensConsumed: totalTokens,
9562
+ activeCountriesCount: countriesSet.size,
9563
+ marketOpportunities
9564
+ };
9565
+ }
9566
+ }
9567
+
9432
9568
  // src/index.ts
9433
9569
  function createLynxSeoEngine(config) {
9434
9570
  return new LynxSeoEngine(config);
@@ -9478,7 +9614,8 @@ var LynxSeo = {
9478
9614
  translator: LynxRateLimitedTranslator,
9479
9615
  pageRankGraph: InternalPageRankEngine,
9480
9616
  geoCitation: GeoCitationScorer,
9481
- marketingSkills: MarketingSkillsEngine
9617
+ marketingSkills: MarketingSkillsEngine,
9618
+ regenerationTracker: PageRegenerationTracker
9482
9619
  };
9483
9620
  var src_default = LynxSeo;
9484
9621
  export {
@@ -9518,6 +9655,7 @@ export {
9518
9655
  PublicRoutesManifestEngine,
9519
9656
  PseoMatrixEngine,
9520
9657
  PowerWordsPsychologyEngine,
9658
+ PageRegenerationTracker,
9521
9659
  PSEO_AGENT_SYSTEM_PROMPT,
9522
9660
  OgImageGenerator,
9523
9661
  NGramDensityAnalyzer,
@@ -131,7 +131,10 @@ export interface MatrixOptions {
131
131
  brandName: string;
132
132
  language?: string;
133
133
  languages?: string[];
134
+ allowedLanguages?: string[] | string;
134
135
  countries?: string[] | string;
136
+ allowedCountries?: string[] | string;
137
+ strictCountryBoundary?: boolean;
135
138
  territories?: string[] | string;
136
139
  cleanDirectRoutes?: boolean;
137
140
  enableFreeTools?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "High-Performance Universal Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",