@lynxflow/seo-engine 2.0.0 โ†’ 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 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.0.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.
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
  }
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
  }
@@ -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.0.1",
4
4
  "description": "High-Performance Universal Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",