@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 +74 -20
- package/dist/i18n-detector.d.ts +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +92 -0
- package/dist/index.mjs +92 -0
- package/dist/matrix-engine.d.ts +28 -0
- package/package.json +1 -1
- package/src/engine.test.ts +31 -0
- package/src/i18n-detector.ts +70 -0
- package/src/index.ts +1 -0
- package/src/matrix-engine.ts +86 -0
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
|
-
|
|
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
|
-
|
|
75
|
-
const pages = matrixEngine.generateAllMatrices("https://example.com", {
|
|
76
|
+
export const PSEO_DATASET = {
|
|
76
77
|
services: [
|
|
77
|
-
{
|
|
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: "
|
|
94
|
+
{ slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] }
|
|
81
95
|
],
|
|
82
96
|
integrations: [
|
|
83
|
-
{ slug: "
|
|
97
|
+
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] }
|
|
84
98
|
],
|
|
85
99
|
templates: [
|
|
86
|
-
{ slug: "
|
|
100
|
+
{ slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Free download"] }
|
|
87
101
|
],
|
|
88
102
|
glossaryTerms: [
|
|
89
|
-
{ slug: "
|
|
103
|
+
{ slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" }
|
|
90
104
|
]
|
|
91
|
-
}
|
|
105
|
+
};
|
|
106
|
+
```
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
144
|
+
### 3. XML Sitemap & `/llms.txt` Feeds (1 Line of Code)
|
|
98
145
|
|
|
99
146
|
```typescript
|
|
100
|
-
|
|
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,
|
package/dist/matrix-engine.d.ts
CHANGED
|
@@ -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
package/src/engine.test.ts
CHANGED
|
@@ -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";
|
package/src/matrix-engine.ts
CHANGED
|
@@ -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
|
}
|