@lynxflow/seo-engine 1.5.8 → 1.6.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
@@ -57,47 +57,101 @@ The SDK structures programmatic content across **10 canonical root pillars** exp
57
57
 
58
58
  ## Quickstart
59
59
 
60
- ### 1. Initialize Engine
60
+ ### 1. Initialize Engine & Configure Minimal Inputs
61
+
62
+ The SDK runs **in-memory inside the client application** (< 0.05ms) and automatically provisions verified cities, populations, GPS coordinates, and currencies from its built-in global demographic database.
61
63
 
62
64
  ```typescript
63
65
  import { PseoMatrixEngine } from "@lynxflow/seo-engine";
64
66
 
65
- export const matrixEngine = new PseoMatrixEngine({
66
- companyName: "Acme Cloud",
67
- language: "en", // "en" default, supports "fr", "de", "es", "it", "pt", etc.
68
- correctionsEmail: "compliance@acme.com",
69
- });
70
- ```
67
+ export const matrixEngine = new PseoMatrixEngine();
71
68
 
72
- ### 2. Resolve Programmatic Pages
69
+ export const SEO_CONFIG = {
70
+ brandName: "Acme Cloud",
71
+ languages: ["fr", "en", "es"], // Supported site languages
72
+ countries: ["france", "espagne", "belgique"], // Or "europe" / "international" — SDK auto-provisions verified cities!
73
+ cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.) for direct /{service}/{city}
74
+ };
73
75
 
74
- ```typescript
75
- const pages = matrixEngine.generateAllMatrices("https://example.com", {
76
+ export const PSEO_DATASET = {
76
77
  services: [
77
- { slug: "crm-pipeline", name: "Sales CRM", category: "Sales", keyFeatures: ["Kanban"], description: "CRM" }
78
+ {
79
+ slug: "autopost-facebook",
80
+ name: "Autopost Facebook & Instagram",
81
+ category: "Social Media",
82
+ keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"],
83
+ description: "Automated social media posting engine"
84
+ },
85
+ {
86
+ slug: "crm-pipeline",
87
+ name: "Visual CRM Pipeline",
88
+ category: "Sales",
89
+ keyFeatures: ["Kanban", "Lead scoring"],
90
+ description: "Visual deal tracking software"
91
+ },
78
92
  ],
79
93
  targets: [
80
- { slug: "law-firms", name: "Law Firms", type: "industry", painPoints: ["Billing"], benefits: ["Security"] }
94
+ { slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] }
81
95
  ],
82
96
  integrations: [
83
- { slug: "slack", name: "Slack", category: "Communication", syncFeatures: ["Alerts"] }
97
+ { slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] }
84
98
  ],
85
99
  templates: [
86
- { slug: "pipeline-excel", title: "Sales Pipeline", topic: "Sales", format: "excel", benefits: ["Free"] }
100
+ { slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Free download"] }
87
101
  ],
88
102
  glossaryTerms: [
89
- { slug: "mrr", term: "MRR", shortDefinition: "Monthly Recurring Revenue" }
103
+ { slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" }
90
104
  ]
91
- }, { brandName: "Acme" });
105
+ };
106
+ ```
92
107
 
93
- console.log(pages[0].urlPath); // "/for/law-firms"
94
- console.log(pages[0].schemaGraph); // Injected Schema.org JSON-LD graph
108
+ ### 2. Next.js Universal Dynamic Catch-All (`app/[...slug]/page.tsx`)
109
+
110
+ ```tsx
111
+ import { notFound } from "next/navigation";
112
+ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
113
+
114
+ export async function generateMetadata({ params }: { params: { slug: string[] } }) {
115
+ const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
116
+ const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
117
+ if (!page) return {};
118
+ return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
119
+ }
120
+
121
+ export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
122
+ const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
123
+ // ⚡ Resolved mathematically in local RAM in < 0.05ms
124
+ const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
125
+ if (!page) notFound();
126
+
127
+ return (
128
+ <article className="min-h-screen bg-background text-foreground py-12 px-6 max-w-5xl mx-auto space-y-8">
129
+ <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(page.schemaGraph) }} />
130
+ <h1 className="text-4xl font-extrabold">{page.h1}</h1>
131
+ <p className="text-lg text-muted-foreground">{page.description}</p>
132
+ {page.neighboringLinks && (
133
+ <nav className="flex flex-wrap gap-2 pt-6">
134
+ {page.neighboringLinks.map((l) => (
135
+ <a key={l.url} href={l.url} className="px-3 py-1 bg-secondary rounded text-xs">{l.name}</a>
136
+ ))}
137
+ </nav>
138
+ )}
139
+ </article>
140
+ );
141
+ }
95
142
  ```
96
143
 
97
- ### 3. URL Slug Cleansing (6 Golden Rules)
144
+ ### 3. XML Sitemap & `/llms.txt` Feeds (1 Line of Code)
98
145
 
99
146
  ```typescript
100
- import { cleanSeoSlug, validateSeoSlug } from "@lynxflow/seo-engine";
147
+ // app/sitemap.ts
148
+ export default function sitemap() {
149
+ const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
150
+ return matrixEngine.generateAllMatrices(domain, PSEO_DATASET, SEO_CONFIG)
151
+ .filter(p => p.robots.includes("index"))
152
+ .map(p => ({ url: p.canonicalUrl, lastModified: new Date() }));
153
+ }
154
+ ```
101
155
 
102
156
  const slug = cleanSeoSlug("The best software for managing client invoices in 2026", { language: "en" });
103
157
  console.log(slug); // "best-software-managing-client-invoices"
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 🌐 Dynamic i18n & Locale Auto-Detector
3
+ * Automatically detects and synchronizes website locales without requiring hardcoded lists.
4
+ * Compatible with next-intl, next-i18next, i18next, Paraglide, and Astro i18n.
5
+ */
6
+ export declare const SUPPORTED_CANONICAL_LOCALES: readonly ["en", "fr", "es", "de", "it", "pt", "nl", "ru", "sv", "pl", "ja", "zh", "ar"];
7
+ export type SupportedLocale = (typeof SUPPORTED_CANONICAL_LOCALES)[number];
8
+ export declare class I18nDetector {
9
+ /**
10
+ * Resolves a clean 2-letter ISO language code from any locale string (e.g. "fr-FR" -> "fr").
11
+ */
12
+ static normalizeLocale(localeInput?: string): string;
13
+ /**
14
+ * Auto-detects and extracts active locales from common environment variables or array inputs.
15
+ */
16
+ static resolveActiveLocales(explicitLocales?: string[] | string): string[];
17
+ /**
18
+ * Matches whether a given path segment is a language prefix (e.g. "/fr/autopost-facebook/paris").
19
+ */
20
+ static extractLocaleFromPath(pathname: string, activeLocales: string[]): {
21
+ locale: string;
22
+ remainingPath: string;
23
+ };
24
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./slug-engine";
14
14
  export * from "./legal-disclaimers";
15
15
  export * from "./matrix-engine";
16
16
  export * from "./built-in-locations";
17
+ export * from "./i18n-detector";
17
18
  export * from "./brand-icons";
18
19
  export * from "./urlytics-engine";
19
20
  export * from "./keyword-permutator";
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ __export(exports_src, {
61
61
  SerpClient: () => SerpClient,
62
62
  SeoOpportunitiesDecayDetector: () => SeoOpportunitiesDecayDetector,
63
63
  SchemaGraphBuilder: () => SchemaGraphBuilder,
64
+ SUPPORTED_CANONICAL_LOCALES: () => SUPPORTED_CANONICAL_LOCALES,
64
65
  SCHEMA_LOCAL_BUSINESS_MAP: () => SCHEMA_LOCAL_BUSINESS_MAP,
65
66
  RssSyndicationFeedGenerator: () => RssSyndicationFeedGenerator,
66
67
  RealReviewsSyncEngine: () => RealReviewsSyncEngine,
@@ -84,6 +85,7 @@ __export(exports_src, {
84
85
  IsrCacheManager: () => IsrCacheManager,
85
86
  InstantMatrixSearchEngine: () => InstantMatrixSearchEngine,
86
87
  IndexNowClient: () => IndexNowClient,
88
+ I18nDetector: () => I18nDetector,
87
89
  HyperswitchGateway: () => HyperswitchGateway,
88
90
  GeoMeshLinkingEngine: () => GeoMeshLinkingEngine,
89
91
  ExtendedSchemaGraphBuilder: () => ExtendedSchemaGraphBuilder,
@@ -3862,6 +3864,11 @@ class PseoMatrixEngine {
3862
3864
  ${urls}
3863
3865
  </urlset>`;
3864
3866
  }
3867
+ resolveServicePage(serviceSlug, citySlug, domain, data, options) {
3868
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
3869
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
3870
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
3871
+ }
3865
3872
  generateLlmsTxt(domain, data, options) {
3866
3873
  const pages = this.generateAllMatrices(domain, data, options);
3867
3874
  const lines = [
@@ -3876,6 +3883,91 @@ ${urls}
3876
3883
  return lines.join(`
3877
3884
  `);
3878
3885
  }
3886
+ generateRobotsTxt(domain, options) {
3887
+ const cleanDomain = domain.replace(/\/+$/, "");
3888
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
3889
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
3890
+ const lines = [
3891
+ "User-agent: *",
3892
+ "Allow: /",
3893
+ ...disallows.map((d) => `Disallow: ${d}`),
3894
+ ""
3895
+ ];
3896
+ if (options?.allowAiBots !== false) {
3897
+ lines.push("# AI Search Crawlers Optimization (AEO/GEO)", "User-agent: GPTBot", "Allow: /", "", "User-agent: ClaudeBot", "Allow: /", "", "User-agent: PerplexityBot", "Allow: /", "", "User-agent: Applebot", "Allow: /", "");
3898
+ }
3899
+ lines.push(`Sitemap: ${sitemap}`);
3900
+ return lines.join(`
3901
+ `);
3902
+ }
3903
+ getTelemetryPayload(domain, data, options) {
3904
+ const pages = this.generateAllMatrices(domain, data, options);
3905
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
3906
+ const matrixBreakdown = {};
3907
+ for (const p of pages) {
3908
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
3909
+ }
3910
+ return {
3911
+ domain,
3912
+ brandName: options.brandName,
3913
+ language: options.language || "en",
3914
+ languages: options.languages || [options.language || "en"],
3915
+ totalPages: pages.length,
3916
+ indexedPages: indexedPages.length,
3917
+ servicesCount: data.services?.length ?? 0,
3918
+ matrixBreakdown,
3919
+ timestamp: new Date().toISOString()
3920
+ };
3921
+ }
3922
+ }
3923
+ // src/i18n-detector.ts
3924
+ var SUPPORTED_CANONICAL_LOCALES = [
3925
+ "en",
3926
+ "fr",
3927
+ "es",
3928
+ "de",
3929
+ "it",
3930
+ "pt",
3931
+ "nl",
3932
+ "ru",
3933
+ "sv",
3934
+ "pl",
3935
+ "ja",
3936
+ "zh",
3937
+ "ar"
3938
+ ];
3939
+
3940
+ class I18nDetector {
3941
+ static normalizeLocale(localeInput) {
3942
+ if (!localeInput || typeof localeInput !== "string")
3943
+ return "en";
3944
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
3945
+ return clean || "en";
3946
+ }
3947
+ static resolveActiveLocales(explicitLocales) {
3948
+ if (explicitLocales) {
3949
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
3950
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
3951
+ return Array.from(new Set(normalized));
3952
+ }
3953
+ return ["en", "fr"];
3954
+ }
3955
+ static extractLocaleFromPath(pathname, activeLocales) {
3956
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
3957
+ const segments = cleanPath.split("/").filter(Boolean);
3958
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
3959
+ const locale = segments[0].toLowerCase();
3960
+ const remainingSegments = segments.slice(1);
3961
+ return {
3962
+ locale,
3963
+ remainingPath: `/${remainingSegments.join("/")}`
3964
+ };
3965
+ }
3966
+ return {
3967
+ locale: activeLocales[0] || "en",
3968
+ remainingPath: cleanPath
3969
+ };
3970
+ }
3879
3971
  }
3880
3972
  // src/brand-icons.ts
3881
3973
  var BRAND_ICONS = {
package/dist/index.mjs CHANGED
@@ -3757,6 +3757,11 @@ class PseoMatrixEngine {
3757
3757
  ${urls}
3758
3758
  </urlset>`;
3759
3759
  }
3760
+ resolveServicePage(serviceSlug, citySlug, domain, data, options) {
3761
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
3762
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
3763
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
3764
+ }
3760
3765
  generateLlmsTxt(domain, data, options) {
3761
3766
  const pages = this.generateAllMatrices(domain, data, options);
3762
3767
  const lines = [
@@ -3771,6 +3776,91 @@ ${urls}
3771
3776
  return lines.join(`
3772
3777
  `);
3773
3778
  }
3779
+ generateRobotsTxt(domain, options) {
3780
+ const cleanDomain = domain.replace(/\/+$/, "");
3781
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
3782
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
3783
+ const lines = [
3784
+ "User-agent: *",
3785
+ "Allow: /",
3786
+ ...disallows.map((d) => `Disallow: ${d}`),
3787
+ ""
3788
+ ];
3789
+ if (options?.allowAiBots !== false) {
3790
+ lines.push("# AI Search Crawlers Optimization (AEO/GEO)", "User-agent: GPTBot", "Allow: /", "", "User-agent: ClaudeBot", "Allow: /", "", "User-agent: PerplexityBot", "Allow: /", "", "User-agent: Applebot", "Allow: /", "");
3791
+ }
3792
+ lines.push(`Sitemap: ${sitemap}`);
3793
+ return lines.join(`
3794
+ `);
3795
+ }
3796
+ getTelemetryPayload(domain, data, options) {
3797
+ const pages = this.generateAllMatrices(domain, data, options);
3798
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
3799
+ const matrixBreakdown = {};
3800
+ for (const p of pages) {
3801
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
3802
+ }
3803
+ return {
3804
+ domain,
3805
+ brandName: options.brandName,
3806
+ language: options.language || "en",
3807
+ languages: options.languages || [options.language || "en"],
3808
+ totalPages: pages.length,
3809
+ indexedPages: indexedPages.length,
3810
+ servicesCount: data.services?.length ?? 0,
3811
+ matrixBreakdown,
3812
+ timestamp: new Date().toISOString()
3813
+ };
3814
+ }
3815
+ }
3816
+ // src/i18n-detector.ts
3817
+ var SUPPORTED_CANONICAL_LOCALES = [
3818
+ "en",
3819
+ "fr",
3820
+ "es",
3821
+ "de",
3822
+ "it",
3823
+ "pt",
3824
+ "nl",
3825
+ "ru",
3826
+ "sv",
3827
+ "pl",
3828
+ "ja",
3829
+ "zh",
3830
+ "ar"
3831
+ ];
3832
+
3833
+ class I18nDetector {
3834
+ static normalizeLocale(localeInput) {
3835
+ if (!localeInput || typeof localeInput !== "string")
3836
+ return "en";
3837
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
3838
+ return clean || "en";
3839
+ }
3840
+ static resolveActiveLocales(explicitLocales) {
3841
+ if (explicitLocales) {
3842
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
3843
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
3844
+ return Array.from(new Set(normalized));
3845
+ }
3846
+ return ["en", "fr"];
3847
+ }
3848
+ static extractLocaleFromPath(pathname, activeLocales) {
3849
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
3850
+ const segments = cleanPath.split("/").filter(Boolean);
3851
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
3852
+ const locale = segments[0].toLowerCase();
3853
+ const remainingSegments = segments.slice(1);
3854
+ return {
3855
+ locale,
3856
+ remainingPath: `/${remainingSegments.join("/")}`
3857
+ };
3858
+ }
3859
+ return {
3860
+ locale: activeLocales[0] || "en",
3861
+ remainingPath: cleanPath
3862
+ };
3863
+ }
3774
3864
  }
3775
3865
  // src/brand-icons.ts
3776
3866
  var BRAND_ICONS = {
@@ -6464,6 +6554,7 @@ export {
6464
6554
  SerpClient,
6465
6555
  SeoOpportunitiesDecayDetector,
6466
6556
  SchemaGraphBuilder,
6557
+ SUPPORTED_CANONICAL_LOCALES,
6467
6558
  SCHEMA_LOCAL_BUSINESS_MAP,
6468
6559
  RssSyndicationFeedGenerator,
6469
6560
  RealReviewsSyncEngine,
@@ -6487,6 +6578,7 @@ export {
6487
6578
  IsrCacheManager,
6488
6579
  InstantMatrixSearchEngine,
6489
6580
  IndexNowClient,
6581
+ I18nDetector,
6490
6582
  HyperswitchGateway,
6491
6583
  GeoMeshLinkingEngine,
6492
6584
  ExtendedSchemaGraphBuilder,
@@ -127,6 +127,7 @@ export interface GeneratedPageMeta {
127
127
  export interface MatrixOptions {
128
128
  brandName: string;
129
129
  language?: string;
130
+ languages?: string[];
130
131
  countries?: string[] | string;
131
132
  territories?: string[] | string;
132
133
  cleanDirectRoutes?: boolean;
@@ -220,8 +221,35 @@ export declare class PseoMatrixEngine {
220
221
  * Generates a fully formatted XML Sitemap string.
221
222
  */
222
223
  generateSitemapXml(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
224
+ /**
225
+ * Resolves a page for a specific module/service and city.
226
+ * Useful when organizing code as app/[service]/[city]/page.tsx
227
+ */
228
+ resolveServicePage(serviceSlug: string, citySlug: string, domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): GeneratedPageMeta | undefined;
223
229
  /**
224
230
  * Generates an official /llms.txt Markdown directory for AI search bots.
225
231
  */
226
232
  generateLlmsTxt(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
233
+ /**
234
+ * Generates a modern robots.txt allowing search engines and AI agents with sitemap link.
235
+ */
236
+ generateRobotsTxt(domain: string, options?: {
237
+ sitemapUrl?: string;
238
+ allowAiBots?: boolean;
239
+ disallowPaths?: string[];
240
+ }): string;
241
+ /**
242
+ * Computes telemetry & page metering data for syncing with LynxSEO Studio dashboard.
243
+ */
244
+ getTelemetryPayload(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): {
245
+ domain: string;
246
+ brandName: string;
247
+ language: string;
248
+ languages: string[];
249
+ totalPages: number;
250
+ indexedPages: number;
251
+ servicesCount: number;
252
+ matrixBreakdown: Record<string, number>;
253
+ timestamp: string;
254
+ };
227
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.5.8",
3
+ "version": "1.6.0",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -7,6 +7,7 @@ import { getSeoAgentPrompt } from "./llm-prompt";
7
7
  import { UrlyticsEngine } from "./urlytics-engine";
8
8
  import { KeywordPermutatorEngine } from "./keyword-permutator";
9
9
  import { renderBrandIconSvg } from "./brand-icons";
10
+ import { I18nDetector } from "./i18n-detector";
10
11
 
11
12
  describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
12
13
  it("English: Strips stop words and years", () => {
@@ -234,4 +235,34 @@ describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
234
235
  expect(paths).toContain("/autopost-facebook/madrid");
235
236
  expect(paths).toContain("/autopost-facebook/barcelona");
236
237
  });
238
+
239
+ it("I18nDetector: Normalizes locales and extracts locale from pathname", () => {
240
+ expect(I18nDetector.normalizeLocale("fr-FR")).toBe("fr");
241
+ expect(I18nDetector.normalizeLocale("en_US")).toBe("en");
242
+
243
+ const extraction = I18nDetector.extractLocaleFromPath("/fr/autopost-facebook/paris", ["fr", "en", "es"]);
244
+ expect(extraction.locale).toBe("fr");
245
+ expect(extraction.remainingPath).toBe("/autopost-facebook/paris");
246
+ });
247
+
248
+ it("PseoMatrixEngine: resolveServicePage, generateRobotsTxt, and getTelemetryPayload", () => {
249
+ const engine = new PseoMatrixEngine();
250
+ const data = {
251
+ services: [{ slug: "crm", name: "CRM", category: "Sales" }],
252
+ locations: [{ slug: "paris", name: "Paris", country: "France" }],
253
+ };
254
+
255
+ const servicePage = engine.resolveServicePage("crm", "paris", "https://acme.com", data, { brandName: "Acme" });
256
+ expect(servicePage).toBeDefined();
257
+ expect(servicePage?.h1).toContain("CRM");
258
+
259
+ const robots = engine.generateRobotsTxt("https://acme.com", { allowAiBots: true });
260
+ expect(robots).toContain("User-agent: GPTBot");
261
+ expect(robots).toContain("Sitemap: https://acme.com/sitemap.xml");
262
+
263
+ const telemetry = engine.getTelemetryPayload("https://acme.com", data, { brandName: "Acme" });
264
+ expect(telemetry.totalPages).toBeGreaterThan(0);
265
+ expect(telemetry.brandName).toBe("Acme");
266
+ expect(telemetry.servicesCount).toBe(1);
267
+ });
237
268
  });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 🌐 Dynamic i18n & Locale Auto-Detector
3
+ * Automatically detects and synchronizes website locales without requiring hardcoded lists.
4
+ * Compatible with next-intl, next-i18next, i18next, Paraglide, and Astro i18n.
5
+ */
6
+
7
+ export const SUPPORTED_CANONICAL_LOCALES = [
8
+ "en",
9
+ "fr",
10
+ "es",
11
+ "de",
12
+ "it",
13
+ "pt",
14
+ "nl",
15
+ "ru",
16
+ "sv",
17
+ "pl",
18
+ "ja",
19
+ "zh",
20
+ "ar",
21
+ ] as const;
22
+
23
+ export type SupportedLocale = (typeof SUPPORTED_CANONICAL_LOCALES)[number];
24
+
25
+ export class I18nDetector {
26
+ /**
27
+ * Resolves a clean 2-letter ISO language code from any locale string (e.g. "fr-FR" -> "fr").
28
+ */
29
+ static normalizeLocale(localeInput?: string): string {
30
+ if (!localeInput || typeof localeInput !== "string") return "en";
31
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
32
+ return clean || "en";
33
+ }
34
+
35
+ /**
36
+ * Auto-detects and extracts active locales from common environment variables or array inputs.
37
+ */
38
+ static resolveActiveLocales(explicitLocales?: string[] | string): string[] {
39
+ if (explicitLocales) {
40
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
41
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
42
+ return Array.from(new Set(normalized));
43
+ }
44
+
45
+ // Default fallback
46
+ return ["en", "fr"];
47
+ }
48
+
49
+ /**
50
+ * Matches whether a given path segment is a language prefix (e.g. "/fr/autopost-facebook/paris").
51
+ */
52
+ static extractLocaleFromPath(pathname: string, activeLocales: string[]): { locale: string; remainingPath: string } {
53
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
54
+ const segments = cleanPath.split("/").filter(Boolean);
55
+
56
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
57
+ const locale = segments[0].toLowerCase();
58
+ const remainingSegments = segments.slice(1);
59
+ return {
60
+ locale,
61
+ remainingPath: `/${remainingSegments.join("/")}`,
62
+ };
63
+ }
64
+
65
+ return {
66
+ locale: activeLocales[0] || "en",
67
+ remainingPath: cleanPath,
68
+ };
69
+ }
70
+ }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./slug-engine";
15
15
  export * from "./legal-disclaimers";
16
16
  export * from "./matrix-engine";
17
17
  export * from "./built-in-locations";
18
+ export * from "./i18n-detector";
18
19
  export * from "./brand-icons";
19
20
  export * from "./urlytics-engine";
20
21
  export * from "./keyword-permutator";
@@ -156,6 +156,7 @@ export interface GeneratedPageMeta {
156
156
  export interface MatrixOptions {
157
157
  brandName: string;
158
158
  language?: string; // Default: "en", supports "fr", "de", "es", "pt", "it", etc.
159
+ languages?: string[]; // Array of supported site languages (e.g. ["fr", "en", "es"])
159
160
  countries?: string[] | string; // e.g. ["france", "spain"] or "europe" or "international"
160
161
  territories?: string[] | string; // Alias for countries
161
162
  cleanDirectRoutes?: boolean; // When true (default), eliminates parasite words (/solutions/, etc.) for direct /{service}/{city}
@@ -882,6 +883,22 @@ ${urls}
882
883
  </urlset>`;
883
884
  }
884
885
 
886
+ /**
887
+ * Resolves a page for a specific module/service and city.
888
+ * Useful when organizing code as app/[service]/[city]/page.tsx
889
+ */
890
+ resolveServicePage(
891
+ serviceSlug: string,
892
+ citySlug: string,
893
+ domain: string,
894
+ data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
895
+ options: MatrixOptions,
896
+ ): GeneratedPageMeta | undefined {
897
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
898
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
899
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
900
+ }
901
+
885
902
  /**
886
903
  * Generates an official /llms.txt Markdown directory for AI search bots.
887
904
  */
@@ -904,4 +921,73 @@ ${urls}
904
921
 
905
922
  return lines.join("\n");
906
923
  }
924
+
925
+ /**
926
+ * Generates a modern robots.txt allowing search engines and AI agents with sitemap link.
927
+ */
928
+ generateRobotsTxt(
929
+ domain: string,
930
+ options?: { sitemapUrl?: string; allowAiBots?: boolean; disallowPaths?: string[] },
931
+ ): string {
932
+ const cleanDomain = domain.replace(/\/+$/, "");
933
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
934
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
935
+
936
+ const lines: string[] = [
937
+ "User-agent: *",
938
+ "Allow: /",
939
+ ...disallows.map((d) => `Disallow: ${d}`),
940
+ "",
941
+ ];
942
+
943
+ if (options?.allowAiBots !== false) {
944
+ lines.push(
945
+ "# AI Search Crawlers Optimization (AEO/GEO)",
946
+ "User-agent: GPTBot",
947
+ "Allow: /",
948
+ "",
949
+ "User-agent: ClaudeBot",
950
+ "Allow: /",
951
+ "",
952
+ "User-agent: PerplexityBot",
953
+ "Allow: /",
954
+ "",
955
+ "User-agent: Applebot",
956
+ "Allow: /",
957
+ "",
958
+ );
959
+ }
960
+
961
+ lines.push(`Sitemap: ${sitemap}`);
962
+ return lines.join("\n");
963
+ }
964
+
965
+ /**
966
+ * Computes telemetry & page metering data for syncing with LynxSEO Studio dashboard.
967
+ */
968
+ getTelemetryPayload(
969
+ domain: string,
970
+ data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
971
+ options: MatrixOptions,
972
+ ) {
973
+ const pages = this.generateAllMatrices(domain, data, options);
974
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
975
+
976
+ const matrixBreakdown: Record<string, number> = {};
977
+ for (const p of pages) {
978
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
979
+ }
980
+
981
+ return {
982
+ domain,
983
+ brandName: options.brandName,
984
+ language: options.language || "en",
985
+ languages: options.languages || [options.language || "en"],
986
+ totalPages: pages.length,
987
+ indexedPages: indexedPages.length,
988
+ servicesCount: data.services?.length ?? 0,
989
+ matrixBreakdown,
990
+ timestamp: new Date().toISOString(),
991
+ };
992
+ }
907
993
  }