@lynxflow/seo-engine 1.5.6 → 1.5.8
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/dist/built-in-locations.d.ts +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +214 -20
- package/dist/index.mjs +214 -20
- package/dist/matrix-engine.d.ts +26 -0
- package/package.json +1 -1
- package/src/built-in-locations.ts +170 -0
- package/src/engine.test.ts +48 -0
- package/src/index.ts +1 -0
- package/src/llm-prompt.ts +30 -17
- package/src/matrix-engine.ts +92 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🌍 Built-in Global Geographic & Demographic Database
|
|
3
|
+
* Automatically provisions verified cities, populations, currencies, and coordinates
|
|
4
|
+
* across Europe, North America, and international markets without requiring the user to list cities manually.
|
|
5
|
+
*/
|
|
6
|
+
import { PseoLocation } from "./matrix-engine";
|
|
7
|
+
export declare const BUILT_IN_LOCATIONS_DATABASE: Record<string, PseoLocation[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves built-in locations by country code or territory keyword.
|
|
10
|
+
* Supports: "fr", "france", "es", "spain", "de", "germany", "uk", "us", "be", "ch", "it", "ca", "europe", "international", "all"
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveBuiltInLocations(territories?: string[] | string): PseoLocation[];
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./engine";
|
|
|
13
13
|
export * from "./slug-engine";
|
|
14
14
|
export * from "./legal-disclaimers";
|
|
15
15
|
export * from "./matrix-engine";
|
|
16
|
+
export * from "./built-in-locations";
|
|
16
17
|
export * from "./brand-icons";
|
|
17
18
|
export * from "./urlytics-engine";
|
|
18
19
|
export * from "./keyword-permutator";
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ var __export = (target, all) => {
|
|
|
40
40
|
var exports_src = {};
|
|
41
41
|
__export(exports_src, {
|
|
42
42
|
validateSeoSlug: () => validateSeoSlug,
|
|
43
|
+
resolveBuiltInLocations: () => resolveBuiltInLocations,
|
|
43
44
|
resolveBrandIcon: () => resolveBrandIcon,
|
|
44
45
|
renderBrandIconSvg: () => renderBrandIconSvg,
|
|
45
46
|
getSeoAgentPrompt: () => getSeoAgentPrompt,
|
|
@@ -93,6 +94,7 @@ __export(exports_src, {
|
|
|
93
94
|
CopywritingFrameworksMaster: () => CopywritingFrameworksMaster,
|
|
94
95
|
BrandDnaCalendarEngine: () => BrandDnaCalendarEngine,
|
|
95
96
|
BacklinksClient: () => BacklinksClient,
|
|
97
|
+
BUILT_IN_LOCATIONS_DATABASE: () => BUILT_IN_LOCATIONS_DATABASE,
|
|
96
98
|
BRAND_ICONS: () => BRAND_ICONS,
|
|
97
99
|
ApiKeyGuardian: () => ApiKeyGuardian,
|
|
98
100
|
AiCopilotClient: () => AiCopilotClient,
|
|
@@ -3190,6 +3192,150 @@ class ExtendedSchemaGraphBuilder {
|
|
|
3190
3192
|
}
|
|
3191
3193
|
}
|
|
3192
3194
|
|
|
3195
|
+
// src/built-in-locations.ts
|
|
3196
|
+
var BUILT_IN_LOCATIONS_DATABASE = {
|
|
3197
|
+
fr: [
|
|
3198
|
+
{ slug: "paris", name: "Paris", region: "Île-de-France", country: "France", population: 2161000, currency: "EUR", currencySymbol: "€", latitude: 48.8566, longitude: 2.3522, neighborSlugs: ["boulogne-billancourt", "saint-denis", "montreuil"] },
|
|
3199
|
+
{ slug: "marseille", name: "Marseille", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 873000, currency: "EUR", currencySymbol: "€", latitude: 43.2965, longitude: 5.3698, neighborSlugs: ["aix-en-provence", "aubagne", "toulon"] },
|
|
3200
|
+
{ slug: "lyon", name: "Lyon", region: "Auvergne-Rhône-Alpes", country: "France", population: 522000, currency: "EUR", currencySymbol: "€", latitude: 45.764, longitude: 4.8357, neighborSlugs: ["villeurbanne", "venissieux", "saint-etienne"] },
|
|
3201
|
+
{ slug: "toulouse", name: "Toulouse", region: "Occitanie", country: "France", population: 498000, currency: "EUR", currencySymbol: "€", latitude: 43.6047, longitude: 1.4442, neighborSlugs: ["montauban", "albi", "colomiers"] },
|
|
3202
|
+
{ slug: "nice", name: "Nice", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 343000, currency: "EUR", currencySymbol: "€", latitude: 43.7102, longitude: 7.262, neighborSlugs: ["cannes", "antibes", "monaco"] },
|
|
3203
|
+
{ slug: "nantes", name: "Nantes", region: "Pays de la Loire", country: "France", population: 320000, currency: "EUR", currencySymbol: "€", latitude: 47.2184, longitude: -1.5536, neighborSlugs: ["saint-nazaire", "angers", "rennes"] },
|
|
3204
|
+
{ slug: "montpellier", name: "Montpellier", region: "Occitanie", country: "France", population: 299000, currency: "EUR", currencySymbol: "€", latitude: 43.6108, longitude: 3.8767, neighborSlugs: ["nimes", "beziers", "sete"] },
|
|
3205
|
+
{ slug: "strasbourg", name: "Strasbourg", region: "Grand Est", country: "France", population: 290000, currency: "EUR", currencySymbol: "€", latitude: 48.5734, longitude: 7.7521, neighborSlugs: ["mulhouse", "colmar", "metz"] },
|
|
3206
|
+
{ slug: "bordeaux", name: "Bordeaux", region: "Nouvelle-Aquitaine", country: "France", population: 260000, currency: "EUR", currencySymbol: "€", latitude: 44.8378, longitude: -0.5792, neighborSlugs: ["merignac", "pessac", "arcachon"] },
|
|
3207
|
+
{ slug: "lille", name: "Lille", region: "Hauts-de-France", country: "France", population: 236000, currency: "EUR", currencySymbol: "€", latitude: 50.6292, longitude: 3.0573, neighborSlugs: ["roubaix", "tourcoing", "villeneuve-d-ascq"] },
|
|
3208
|
+
{ slug: "rennes", name: "Rennes", region: "Bretagne", country: "France", population: 222000, currency: "EUR", currencySymbol: "€", latitude: 48.1173, longitude: -1.6778, neighborSlugs: ["saint-malo", "brest", "nantes"] },
|
|
3209
|
+
{ slug: "toulon", name: "Toulon", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 179000, currency: "EUR", currencySymbol: "€", latitude: 43.1242, longitude: 5.928, neighborSlugs: ["hyeres", "la-seyne-sur-mer", "marseille"] },
|
|
3210
|
+
{ slug: "reims", name: "Reims", region: "Grand Est", country: "France", population: 179000, currency: "EUR", currencySymbol: "€", latitude: 49.2583, longitude: 4.0317, neighborSlugs: ["chalons-en-champagne", "troyes", "metz"] },
|
|
3211
|
+
{ slug: "saint-etienne", name: "Saint-Étienne", region: "Auvergne-Rhône-Alpes", country: "France", population: 174000, currency: "EUR", currencySymbol: "€", latitude: 45.4397, longitude: 4.3872, neighborSlugs: ["lyon", "roanne", "saint-chamond"] },
|
|
3212
|
+
{ slug: "le-havre", name: "Le Havre", region: "Normandie", country: "France", population: 166000, currency: "EUR", currencySymbol: "€", latitude: 49.4944, longitude: 0.1079, neighborSlugs: ["rouen", "caen", "honfleur"] },
|
|
3213
|
+
{ slug: "grenoble", name: "Grenoble", region: "Auvergne-Rhône-Alpes", country: "France", population: 158000, currency: "EUR", currencySymbol: "€", latitude: 45.1885, longitude: 5.7245, neighborSlugs: ["echirolles", "chambery", "annecy"] },
|
|
3214
|
+
{ slug: "dijon", name: "Dijon", region: "Bourgogne-Franche-Comté", country: "France", population: 159000, currency: "EUR", currencySymbol: "€", latitude: 47.322, longitude: 5.0415, neighborSlugs: ["besancon", "chalon-sur-saone", "beaune"] },
|
|
3215
|
+
{ slug: "angers", name: "Angers", region: "Pays de la Loire", country: "France", population: 155000, currency: "EUR", currencySymbol: "€", latitude: 47.4784, longitude: -0.5632, neighborSlugs: ["saumur", "cholet", "nantes"] },
|
|
3216
|
+
{ slug: "villeurbanne", name: "Villeurbanne", region: "Auvergne-Rhône-Alpes", country: "France", population: 154000, currency: "EUR", currencySymbol: "€", latitude: 45.7667, longitude: 4.8833, neighborSlugs: ["lyon", "bron", "caluire-et-cuire"] },
|
|
3217
|
+
{ slug: "nimes", name: "Nîmes", region: "Occitanie", country: "France", population: 148000, currency: "EUR", currencySymbol: "€", latitude: 43.8367, longitude: 4.3601, neighborSlugs: ["montpellier", "avignon", "arles"] },
|
|
3218
|
+
{ slug: "aix-en-provence", name: "Aix-en-Provence", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 147000, currency: "EUR", currencySymbol: "€", latitude: 43.5297, longitude: 5.4474, neighborSlugs: ["marseille", "gardanne", "salon-de-provence"] },
|
|
3219
|
+
{ slug: "clermont-ferrand", name: "Clermont-Ferrand", region: "Auvergne-Rhône-Alpes", country: "France", population: 147000, currency: "EUR", currencySymbol: "€", latitude: 45.7772, longitude: 3.087, neighborSlugs: ["riom", "cournon-d-auvergne", "vichy"] },
|
|
3220
|
+
{ slug: "brest", name: "Brest", region: "Bretagne", country: "France", population: 139000, currency: "EUR", currencySymbol: "€", latitude: 48.3904, longitude: -4.4861, neighborSlugs: ["quimper", "morlaix", "rennes"] },
|
|
3221
|
+
{ slug: "tours", name: "Tours", region: "Centre-Val de Loire", country: "France", population: 137000, currency: "EUR", currencySymbol: "€", latitude: 47.3941, longitude: 0.6848, neighborSlugs: ["orleans", "blois", "angers"] },
|
|
3222
|
+
{ slug: "amiens", name: "Amiens", region: "Hauts-de-France", country: "France", population: 134000, currency: "EUR", currencySymbol: "€", latitude: 49.8941, longitude: 2.2958, neighborSlugs: ["beauvais", "compiegne", "lille"] },
|
|
3223
|
+
{ slug: "annecy", name: "Annecy", region: "Auvergne-Rhône-Alpes", country: "France", population: 131000, currency: "EUR", currencySymbol: "€", latitude: 45.8992, longitude: 6.1294, neighborSlugs: ["chambery", "geneve", "grenoble"] }
|
|
3224
|
+
],
|
|
3225
|
+
es: [
|
|
3226
|
+
{ slug: "madrid", name: "Madrid", region: "Comunidad de Madrid", country: "Spain", population: 3300000, currency: "EUR", currencySymbol: "€", latitude: 40.4168, longitude: -3.7038, neighborSlugs: ["alcala-de-henares", "mostoles", "getafe"] },
|
|
3227
|
+
{ slug: "barcelona", name: "Barcelona", region: "Catalunya", country: "Spain", population: 1636000, currency: "EUR", currencySymbol: "€", latitude: 41.3851, longitude: 2.1734, neighborSlugs: ["hospitalet-de-llobregat", "badalona", "terrassa"] },
|
|
3228
|
+
{ slug: "valencia", name: "Valencia", region: "Comunitat Valenciana", country: "Spain", population: 800000, currency: "EUR", currencySymbol: "€", latitude: 39.4699, longitude: -0.3763, neighborSlugs: ["alicante", "castellon-de-la-plana", "gandia"] },
|
|
3229
|
+
{ slug: "sevilla", name: "Sevilla", region: "Andalucía", country: "Spain", population: 688000, currency: "EUR", currencySymbol: "€", latitude: 37.3891, longitude: -5.9845, neighborSlugs: ["cordoba", "huelva", "cadiz"] },
|
|
3230
|
+
{ slug: "zaragoza", name: "Zaragoza", region: "Aragón", country: "Spain", population: 675000, currency: "EUR", currencySymbol: "€", latitude: 41.6488, longitude: -0.8891, neighborSlugs: ["huesca", "teruel", "madrid"] },
|
|
3231
|
+
{ slug: "malaga", name: "Málaga", region: "Andalucía", country: "Spain", population: 579000, currency: "EUR", currencySymbol: "€", latitude: 36.7213, longitude: -4.4214, neighborSlugs: ["marbella", "torremolinos", "granada"] },
|
|
3232
|
+
{ slug: "bilbao", name: "Bilbao", region: "País Vasco", country: "Spain", population: 346000, currency: "EUR", currencySymbol: "€", latitude: 43.263, longitude: -2.935, neighborSlugs: ["san-sebastian", "vitoria-gasteiz", "santander"] }
|
|
3233
|
+
],
|
|
3234
|
+
de: [
|
|
3235
|
+
{ slug: "berlin", name: "Berlin", region: "Berlin", country: "Germany", population: 3645000, currency: "EUR", currencySymbol: "€", latitude: 52.52, longitude: 13.405, neighborSlugs: ["potsdam", "cottbus", "brandenburg"] },
|
|
3236
|
+
{ slug: "hamburg", name: "Hamburg", region: "Hamburg", country: "Germany", population: 1841000, currency: "EUR", currencySymbol: "€", latitude: 53.5511, longitude: 9.9937, neighborSlugs: ["luebeck", "bremen", "kiel"] },
|
|
3237
|
+
{ slug: "muenchen", name: "München", region: "Bayern", country: "Germany", population: 1472000, currency: "EUR", currencySymbol: "€", latitude: 48.1351, longitude: 11.582, neighborSlugs: ["augsburg", "ingolstadt", "rosenheim"] },
|
|
3238
|
+
{ slug: "koeln", name: "Köln", region: "Nordrhein-Westfalen", country: "Germany", population: 1086000, currency: "EUR", currencySymbol: "€", latitude: 50.9375, longitude: 6.9603, neighborSlugs: ["duesseldorf", "bonn", "leverkusen"] },
|
|
3239
|
+
{ slug: "frankfurt", name: "Frankfurt am Main", region: "Hessen", country: "Germany", population: 753000, currency: "EUR", currencySymbol: "€", latitude: 50.1109, longitude: 8.6821, neighborSlugs: ["wiesbaden", "mainz", "darmstadt"] },
|
|
3240
|
+
{ slug: "stuttgart", name: "Stuttgart", region: "Baden-Württemberg", country: "Germany", population: 635000, currency: "EUR", currencySymbol: "€", latitude: 48.7758, longitude: 9.1829, neighborSlugs: ["esslingen", "ludwigsburg", "karlsruhe"] }
|
|
3241
|
+
],
|
|
3242
|
+
uk: [
|
|
3243
|
+
{ slug: "london", name: "London", region: "Greater London", country: "United Kingdom", population: 8982000, currency: "GBP", currencySymbol: "£", latitude: 51.5074, longitude: -0.1278, neighborSlugs: ["croydon", "watford", "reading"] },
|
|
3244
|
+
{ slug: "birmingham", name: "Birmingham", region: "West Midlands", country: "United Kingdom", population: 1149000, currency: "GBP", currencySymbol: "£", latitude: 52.4862, longitude: -1.8904, neighborSlugs: ["coventry", "wolverhampton", "solihull"] },
|
|
3245
|
+
{ slug: "manchester", name: "Manchester", region: "Greater Manchester", country: "United Kingdom", population: 553000, currency: "GBP", currencySymbol: "£", latitude: 53.4808, longitude: -2.2426, neighborSlugs: ["salford", "bolton", "stockport"] },
|
|
3246
|
+
{ slug: "edinburgh", name: "Edinburgh", region: "Scotland", country: "United Kingdom", population: 527000, currency: "GBP", currencySymbol: "£", latitude: 55.9533, longitude: -3.1883, neighborSlugs: ["glasgow", "livingston", "dundee"] }
|
|
3247
|
+
],
|
|
3248
|
+
us: [
|
|
3249
|
+
{ slug: "new-york", name: "New York", region: "NY", country: "United States", population: 8336000, currency: "USD", currencySymbol: "$", latitude: 40.7128, longitude: -74.006, neighborSlugs: ["brooklyn", "queens", "jersey-city"] },
|
|
3250
|
+
{ slug: "los-angeles", name: "Los Angeles", region: "CA", country: "United States", population: 3979000, currency: "USD", currencySymbol: "$", latitude: 34.0522, longitude: -118.2437, neighborSlugs: ["long-beach", "pasadena", "glendale"] },
|
|
3251
|
+
{ slug: "chicago", name: "Chicago", region: "IL", country: "United States", population: 2693000, currency: "USD", currencySymbol: "$", latitude: 41.8781, longitude: -87.6298, neighborSlugs: ["evanston", "naperville", "aurora"] },
|
|
3252
|
+
{ slug: "houston", name: "Houston", region: "TX", country: "United States", population: 2320000, currency: "USD", currencySymbol: "$", latitude: 29.7604, longitude: -95.3698, neighborSlugs: ["the-woodlands", "sugar-land", "katy"] },
|
|
3253
|
+
{ slug: "miami", name: "Miami", region: "FL", country: "United States", population: 442000, currency: "USD", currencySymbol: "$", latitude: 25.7617, longitude: -80.1918, neighborSlugs: ["fort-lauderdale", "miami-beach", "hialeah"] },
|
|
3254
|
+
{ slug: "san-francisco", name: "San Francisco", region: "CA", country: "United States", population: 873000, currency: "USD", currencySymbol: "$", latitude: 37.7749, longitude: -122.4194, neighborSlugs: ["oakland", "san-jose", "berkeley"] }
|
|
3255
|
+
],
|
|
3256
|
+
be: [
|
|
3257
|
+
{ slug: "bruxelles", name: "Bruxelles", region: "Bruxelles-Capitale", country: "Belgium", population: 1220000, currency: "EUR", currencySymbol: "€", latitude: 50.8503, longitude: 4.3517, neighborSlugs: ["anvers", "gand", "liege"] },
|
|
3258
|
+
{ slug: "anvers", name: "Anvers", region: "Flandre", country: "Belgium", population: 530000, currency: "EUR", currencySymbol: "€", latitude: 51.2194, longitude: 4.4025, neighborSlugs: ["bruxelles", "malines", "gand"] },
|
|
3259
|
+
{ slug: "liege", name: "Liège", region: "Wallonie", country: "Belgium", population: 197000, currency: "EUR", currencySymbol: "€", latitude: 50.6326, longitude: 5.5797, neighborSlugs: ["namur", "charleroi", "bruxelles"] }
|
|
3260
|
+
],
|
|
3261
|
+
ch: [
|
|
3262
|
+
{ slug: "zurich", name: "Zürich", region: "Zürich", country: "Switzerland", population: 434000, currency: "CHF", currencySymbol: "CHF", latitude: 47.3769, longitude: 8.5417, neighborSlugs: ["bale", "berne", "lucerne"] },
|
|
3263
|
+
{ slug: "geneve", name: "Genève", region: "Genève", country: "Switzerland", population: 203000, currency: "EUR", currencySymbol: "CHF", latitude: 46.2044, longitude: 6.1432, neighborSlugs: ["lausanne", "annecy", "nyon"] },
|
|
3264
|
+
{ slug: "lausanne", name: "Lausanne", region: "Vaud", country: "Switzerland", population: 140000, currency: "CHF", currencySymbol: "CHF", latitude: 46.5197, longitude: 6.6323, neighborSlugs: ["geneve", "montreux", "vevey"] }
|
|
3265
|
+
],
|
|
3266
|
+
it: [
|
|
3267
|
+
{ slug: "roma", name: "Roma", region: "Lazio", country: "Italy", population: 2873000, currency: "EUR", currencySymbol: "€", latitude: 41.9028, longitude: 12.4964, neighborSlugs: ["latina", "fiumicino", "tivoli"] },
|
|
3268
|
+
{ slug: "milano", name: "Milano", region: "Lombardia", country: "Italy", population: 1366000, currency: "EUR", currencySymbol: "€", latitude: 45.4642, longitude: 9.19, neighborSlugs: ["monza", "bergamo", "brescia"] },
|
|
3269
|
+
{ slug: "torino", name: "Torino", region: "Piemonte", country: "Italy", population: 870000, currency: "EUR", currencySymbol: "€", latitude: 45.0703, longitude: 7.6869, neighborSlugs: ["novara", "alessandria", "asti"] }
|
|
3270
|
+
],
|
|
3271
|
+
ca: [
|
|
3272
|
+
{ slug: "toronto", name: "Toronto", region: "ON", country: "Canada", population: 2794000, currency: "CAD", currencySymbol: "$", latitude: 43.6532, longitude: -79.3832, neighborSlugs: ["mississauga", "brampton", "markham"] },
|
|
3273
|
+
{ slug: "montreal", name: "Montréal", region: "QC", country: "Canada", population: 1762000, currency: "CAD", currencySymbol: "$", latitude: 45.5017, longitude: -73.5673, neighborSlugs: ["laval", "longueuil", "quebec"] },
|
|
3274
|
+
{ slug: "vancouver", name: "Vancouver", region: "BC", country: "Canada", population: 662000, currency: "CAD", currencySymbol: "$", latitude: 49.2827, longitude: -123.1207, neighborSlugs: ["burnaby", "richmond", "surrey"] }
|
|
3275
|
+
]
|
|
3276
|
+
};
|
|
3277
|
+
function resolveBuiltInLocations(territories) {
|
|
3278
|
+
if (!territories) {
|
|
3279
|
+
return [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
3280
|
+
}
|
|
3281
|
+
const requested = (Array.isArray(territories) ? territories : [territories]).map((t) => t.toLowerCase().trim());
|
|
3282
|
+
const results = [];
|
|
3283
|
+
const addedSlugs = new Set;
|
|
3284
|
+
const isAllOrEurope = requested.some((t) => ["europe", "international", "all", "global", "world"].includes(t));
|
|
3285
|
+
if (isAllOrEurope) {
|
|
3286
|
+
for (const list of Object.values(BUILT_IN_LOCATIONS_DATABASE)) {
|
|
3287
|
+
for (const loc of list) {
|
|
3288
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
3289
|
+
addedSlugs.add(loc.slug);
|
|
3290
|
+
results.push(loc);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return results;
|
|
3295
|
+
}
|
|
3296
|
+
const countryAliasMap = {
|
|
3297
|
+
france: "fr",
|
|
3298
|
+
fr: "fr",
|
|
3299
|
+
spain: "es",
|
|
3300
|
+
espagne: "es",
|
|
3301
|
+
es: "es",
|
|
3302
|
+
germany: "de",
|
|
3303
|
+
allemagne: "de",
|
|
3304
|
+
de: "de",
|
|
3305
|
+
uk: "uk",
|
|
3306
|
+
gb: "uk",
|
|
3307
|
+
"united kingdom": "uk",
|
|
3308
|
+
angleterre: "uk",
|
|
3309
|
+
us: "us",
|
|
3310
|
+
usa: "us",
|
|
3311
|
+
"united states": "us",
|
|
3312
|
+
etatsunis: "us",
|
|
3313
|
+
belgium: "be",
|
|
3314
|
+
belgique: "be",
|
|
3315
|
+
be: "be",
|
|
3316
|
+
switzerland: "ch",
|
|
3317
|
+
suisse: "ch",
|
|
3318
|
+
ch: "ch",
|
|
3319
|
+
italy: "it",
|
|
3320
|
+
italie: "it",
|
|
3321
|
+
it: "it",
|
|
3322
|
+
canada: "ca",
|
|
3323
|
+
ca: "ca"
|
|
3324
|
+
};
|
|
3325
|
+
for (const req of requested) {
|
|
3326
|
+
const code = countryAliasMap[req.replace(/[^a-z0-9]/g, "")];
|
|
3327
|
+
if (code && BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
3328
|
+
for (const loc of BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
3329
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
3330
|
+
addedSlugs.add(loc.slug);
|
|
3331
|
+
results.push(loc);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3193
3339
|
// src/matrix-engine.ts
|
|
3194
3340
|
class PseoMatrixEngine {
|
|
3195
3341
|
legalEngine;
|
|
@@ -3272,7 +3418,7 @@ class PseoMatrixEngine {
|
|
|
3272
3418
|
const countryCode = cleanSeoSlug(loc.country, { language: lang });
|
|
3273
3419
|
const citySlug = cleanSeoSlug(loc.slug, { language: lang });
|
|
3274
3420
|
const serviceSlug = cleanSeoSlug(s.slug, { language: lang });
|
|
3275
|
-
const urlPath = `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
3421
|
+
const urlPath = options.cleanDirectRoutes !== false ? `/${serviceSlug}/${citySlug}` : `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
3276
3422
|
const fullUrl = `${cleanDomain}${urlPath}`;
|
|
3277
3423
|
const isIndexed = (loc.population ?? 20000) >= minPop;
|
|
3278
3424
|
const robots = isIndexed ? "index, follow" : "noindex, follow";
|
|
@@ -3667,8 +3813,9 @@ class PseoMatrixEngine {
|
|
|
3667
3813
|
}
|
|
3668
3814
|
generateAllMatrices(domain, data, options) {
|
|
3669
3815
|
const allPages = [];
|
|
3670
|
-
if (data.services
|
|
3671
|
-
|
|
3816
|
+
if (data.services) {
|
|
3817
|
+
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.countries || options.territories || options.language || "fr");
|
|
3818
|
+
allPages.push(...this.generateLocalGeoMatrix(domain, data.services, locs, options));
|
|
3672
3819
|
}
|
|
3673
3820
|
if (data.competitors) {
|
|
3674
3821
|
allPages.push(...this.generateVsMatrix(domain, data.competitors, options));
|
|
@@ -3695,6 +3842,40 @@ class PseoMatrixEngine {
|
|
|
3695
3842
|
}
|
|
3696
3843
|
return allPages;
|
|
3697
3844
|
}
|
|
3845
|
+
resolvePage(slugOrPath, domain, data, options) {
|
|
3846
|
+
const path = typeof slugOrPath === "string" ? slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}` : `/${slugOrPath.join("/")}`;
|
|
3847
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
3848
|
+
return pages.find((p) => p.urlPath === path);
|
|
3849
|
+
}
|
|
3850
|
+
generateSitemapXml(domain, data, options) {
|
|
3851
|
+
const pages = this.generateAllMatrices(domain, data, options).filter((p) => p.robots.includes("index"));
|
|
3852
|
+
const now = new Date().toISOString();
|
|
3853
|
+
const urls = pages.map((p) => ` <url>
|
|
3854
|
+
<loc>${p.canonicalUrl}</loc>
|
|
3855
|
+
<lastmod>${now}</lastmod>
|
|
3856
|
+
<changefreq>weekly</changefreq>
|
|
3857
|
+
<priority>0.8</priority>
|
|
3858
|
+
</url>`).join(`
|
|
3859
|
+
`);
|
|
3860
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3861
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3862
|
+
${urls}
|
|
3863
|
+
</urlset>`;
|
|
3864
|
+
}
|
|
3865
|
+
generateLlmsTxt(domain, data, options) {
|
|
3866
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
3867
|
+
const lines = [
|
|
3868
|
+
`# ${options.brandName} — AI Knowledge Graph & Page Hub`,
|
|
3869
|
+
`> Complete index of services, tools, integrations, and local solution hubs.`,
|
|
3870
|
+
"",
|
|
3871
|
+
`## Indexed Programmatic Hubs (${pages.length} Pages)`
|
|
3872
|
+
];
|
|
3873
|
+
for (const p of pages) {
|
|
3874
|
+
lines.push(`- [${p.h1}](${p.canonicalUrl}): ${p.description}`);
|
|
3875
|
+
}
|
|
3876
|
+
return lines.join(`
|
|
3877
|
+
`);
|
|
3878
|
+
}
|
|
3698
3879
|
}
|
|
3699
3880
|
// src/brand-icons.ts
|
|
3700
3881
|
var BRAND_ICONS = {
|
|
@@ -3984,7 +4165,21 @@ When discovering product modules, features, or services to build the SEO matrice
|
|
|
3984
4165
|
|
|
3985
4166
|
---
|
|
3986
4167
|
|
|
3987
|
-
### \
|
|
4168
|
+
### \uD83D\uDCCF 3. THE GOLDEN RULES OF URL ARCHITECTURE (MAX 3-4 SEGMENTS, ZERO PARASITE WORDS)
|
|
4169
|
+
|
|
4170
|
+
- **Rule A (2 to 3 URL Segments Ideal, 4 Maximum):**
|
|
4171
|
+
- ✅ **2 Segments (Top SEO Performance):** \`site.com/{service}/{city}\` (e.g. \`/autopost-facebook/lyon\`, \`/crm/paris\`).
|
|
4172
|
+
- ✅ **3 Segments (B2B Persona/Hub):** \`site.com/for/{target}/{city}\` or \`site.com/{locale}/{service}/{city}\`.
|
|
4173
|
+
- ❌ **Never 5 to 6 Segments:** Avoid \`/solutions/ai/autopost/facebook/fr/lyon\`.
|
|
4174
|
+
- **Rule B (Eliminate All Parasite Noise Words):**
|
|
4175
|
+
- Banish generic wrapper words like \`/solutions/\`, \`/pages/\`, \`/items/\`. Go direct to user search intent: What you do + Platform + Location.
|
|
4176
|
+
- **Rule C (The SDK Does 100% of the Heavy Lifting):**
|
|
4177
|
+
- The developer/AI only registers 3-4 product features and target cities in \`lib/seo.ts\`.
|
|
4178
|
+
- The SDK automatically resolves routes in memory via \`matrixEngine.resolvePage(params.slug)\`, generates complete Schema.org graphs (LocalBusiness, AggregateRating, AggregateOffer), creates AEO summaries, builds FAQ accordions, and manages mesh linking in < 0.05ms.
|
|
4179
|
+
|
|
4180
|
+
---
|
|
4181
|
+
|
|
4182
|
+
### \uD83C\uDFC6 4. THE ARCHITECTURAL PLAYBOOK OF SAAS TITANS (7M+ PAGES WITH 0 DB BOTTLENECKS)
|
|
3988
4183
|
|
|
3989
4184
|
How category leaders generate millions of high-converting pages without server saturation:
|
|
3990
4185
|
|
|
@@ -3996,18 +4191,18 @@ How category leaders generate millions of high-converting pages without server s
|
|
|
3996
4191
|
|
|
3997
4192
|
2. **The 4 Pillars of Zero-Latency (< 0.05ms) Resolution:**
|
|
3998
4193
|
- **0 Bytes in Database:** Never generate physical static files or query heavy databases per request. Pages are resolved mathematically in RAM upon arrival.
|
|
3999
|
-
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\`
|
|
4000
|
-
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge
|
|
4194
|
+
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\` handles all matrices through one lightweight layout.
|
|
4195
|
+
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge on first request.
|
|
4001
4196
|
- **Automatic Siloing & Mesh Linking:**
|
|
4002
4197
|
- Links to 3 neighboring geographic cities for the same service.
|
|
4003
4198
|
- Links to 4 related professions/industries in the same target category.
|
|
4004
4199
|
- Complete Schema.org BreadcrumbList navigation.
|
|
4005
|
-
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index
|
|
4200
|
+
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index.
|
|
4006
4201
|
- **Dynamic \`/llms.txt\` Feed:** High-density Markdown stream enabling direct indexing by ChatGPT Search & Perplexity.
|
|
4007
4202
|
|
|
4008
4203
|
---
|
|
4009
4204
|
|
|
4010
|
-
### \uD83D\uDEE0️
|
|
4205
|
+
### \uD83D\uDEE0️ 5. AI AGENT IMPLEMENTATION PROTOCOL (STEP-BY-STEP)
|
|
4011
4206
|
|
|
4012
4207
|
When setting up programmatic SEO in any codebase, the AI Agent MUST generate these exact 4 files:
|
|
4013
4208
|
|
|
@@ -4019,27 +4214,30 @@ export const matrixEngine = new PseoMatrixEngine();
|
|
|
4019
4214
|
|
|
4020
4215
|
export const SEO_CONFIG = {
|
|
4021
4216
|
brandName: "Acme",
|
|
4022
|
-
language: "
|
|
4217
|
+
language: "fr",
|
|
4218
|
+
countries: ["france", "belgique", "suisse"], // Or "europe" / "international". The SDK automatically provides verified cities!
|
|
4219
|
+
cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.)
|
|
4023
4220
|
minPopulationToIndex: 15_000,
|
|
4024
4221
|
};
|
|
4025
4222
|
|
|
4026
4223
|
// Discovered EXCLUSIVELY from Frontend UI components
|
|
4224
|
+
// NOTE: DO NOT list cities manually! The SDK auto-provisions them based on your target countries/scope.
|
|
4027
4225
|
export const PSEO_DATASET = {
|
|
4028
4226
|
services: [
|
|
4029
4227
|
// 1 per major frontend feature branch
|
|
4030
|
-
{ slug: "
|
|
4228
|
+
{ slug: "autopost-facebook", name: "Autopost Facebook & Instagram", category: "Social Media", keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"], description: "Automated social media posting engine" },
|
|
4031
4229
|
],
|
|
4032
4230
|
targets: [
|
|
4033
|
-
{ slug: "
|
|
4231
|
+
{ slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] },
|
|
4034
4232
|
],
|
|
4035
4233
|
integrations: [
|
|
4036
|
-
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["
|
|
4234
|
+
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] },
|
|
4037
4235
|
],
|
|
4038
4236
|
templates: [
|
|
4039
|
-
{ slug: "
|
|
4237
|
+
{ slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Instant setup"] },
|
|
4040
4238
|
],
|
|
4041
4239
|
glossaryTerms: [
|
|
4042
|
-
{ slug: "
|
|
4240
|
+
{ slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" },
|
|
4043
4241
|
],
|
|
4044
4242
|
};
|
|
4045
4243
|
\`\`\`
|
|
@@ -4052,19 +4250,15 @@ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
|
|
|
4052
4250
|
import { renderBrandIconSvg } from "@lynxflow/seo-engine";
|
|
4053
4251
|
|
|
4054
4252
|
export async function generateMetadata({ params }: { params: { slug: string[] } }) {
|
|
4055
|
-
const path = "/" + params.slug.join("/");
|
|
4056
4253
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
4057
|
-
const
|
|
4058
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
4254
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
4059
4255
|
if (!page) return {};
|
|
4060
4256
|
return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
|
|
4061
4257
|
}
|
|
4062
4258
|
|
|
4063
4259
|
export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
|
|
4064
|
-
const path = "/" + params.slug.join("/");
|
|
4065
4260
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
4066
|
-
const
|
|
4067
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
4261
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
4068
4262
|
if (!page) notFound();
|
|
4069
4263
|
|
|
4070
4264
|
// Automatic Brand Icon Detection
|
package/dist/index.mjs
CHANGED
|
@@ -3087,6 +3087,150 @@ class ExtendedSchemaGraphBuilder {
|
|
|
3087
3087
|
}
|
|
3088
3088
|
}
|
|
3089
3089
|
|
|
3090
|
+
// src/built-in-locations.ts
|
|
3091
|
+
var BUILT_IN_LOCATIONS_DATABASE = {
|
|
3092
|
+
fr: [
|
|
3093
|
+
{ slug: "paris", name: "Paris", region: "Île-de-France", country: "France", population: 2161000, currency: "EUR", currencySymbol: "€", latitude: 48.8566, longitude: 2.3522, neighborSlugs: ["boulogne-billancourt", "saint-denis", "montreuil"] },
|
|
3094
|
+
{ slug: "marseille", name: "Marseille", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 873000, currency: "EUR", currencySymbol: "€", latitude: 43.2965, longitude: 5.3698, neighborSlugs: ["aix-en-provence", "aubagne", "toulon"] },
|
|
3095
|
+
{ slug: "lyon", name: "Lyon", region: "Auvergne-Rhône-Alpes", country: "France", population: 522000, currency: "EUR", currencySymbol: "€", latitude: 45.764, longitude: 4.8357, neighborSlugs: ["villeurbanne", "venissieux", "saint-etienne"] },
|
|
3096
|
+
{ slug: "toulouse", name: "Toulouse", region: "Occitanie", country: "France", population: 498000, currency: "EUR", currencySymbol: "€", latitude: 43.6047, longitude: 1.4442, neighborSlugs: ["montauban", "albi", "colomiers"] },
|
|
3097
|
+
{ slug: "nice", name: "Nice", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 343000, currency: "EUR", currencySymbol: "€", latitude: 43.7102, longitude: 7.262, neighborSlugs: ["cannes", "antibes", "monaco"] },
|
|
3098
|
+
{ slug: "nantes", name: "Nantes", region: "Pays de la Loire", country: "France", population: 320000, currency: "EUR", currencySymbol: "€", latitude: 47.2184, longitude: -1.5536, neighborSlugs: ["saint-nazaire", "angers", "rennes"] },
|
|
3099
|
+
{ slug: "montpellier", name: "Montpellier", region: "Occitanie", country: "France", population: 299000, currency: "EUR", currencySymbol: "€", latitude: 43.6108, longitude: 3.8767, neighborSlugs: ["nimes", "beziers", "sete"] },
|
|
3100
|
+
{ slug: "strasbourg", name: "Strasbourg", region: "Grand Est", country: "France", population: 290000, currency: "EUR", currencySymbol: "€", latitude: 48.5734, longitude: 7.7521, neighborSlugs: ["mulhouse", "colmar", "metz"] },
|
|
3101
|
+
{ slug: "bordeaux", name: "Bordeaux", region: "Nouvelle-Aquitaine", country: "France", population: 260000, currency: "EUR", currencySymbol: "€", latitude: 44.8378, longitude: -0.5792, neighborSlugs: ["merignac", "pessac", "arcachon"] },
|
|
3102
|
+
{ slug: "lille", name: "Lille", region: "Hauts-de-France", country: "France", population: 236000, currency: "EUR", currencySymbol: "€", latitude: 50.6292, longitude: 3.0573, neighborSlugs: ["roubaix", "tourcoing", "villeneuve-d-ascq"] },
|
|
3103
|
+
{ slug: "rennes", name: "Rennes", region: "Bretagne", country: "France", population: 222000, currency: "EUR", currencySymbol: "€", latitude: 48.1173, longitude: -1.6778, neighborSlugs: ["saint-malo", "brest", "nantes"] },
|
|
3104
|
+
{ slug: "toulon", name: "Toulon", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 179000, currency: "EUR", currencySymbol: "€", latitude: 43.1242, longitude: 5.928, neighborSlugs: ["hyeres", "la-seyne-sur-mer", "marseille"] },
|
|
3105
|
+
{ slug: "reims", name: "Reims", region: "Grand Est", country: "France", population: 179000, currency: "EUR", currencySymbol: "€", latitude: 49.2583, longitude: 4.0317, neighborSlugs: ["chalons-en-champagne", "troyes", "metz"] },
|
|
3106
|
+
{ slug: "saint-etienne", name: "Saint-Étienne", region: "Auvergne-Rhône-Alpes", country: "France", population: 174000, currency: "EUR", currencySymbol: "€", latitude: 45.4397, longitude: 4.3872, neighborSlugs: ["lyon", "roanne", "saint-chamond"] },
|
|
3107
|
+
{ slug: "le-havre", name: "Le Havre", region: "Normandie", country: "France", population: 166000, currency: "EUR", currencySymbol: "€", latitude: 49.4944, longitude: 0.1079, neighborSlugs: ["rouen", "caen", "honfleur"] },
|
|
3108
|
+
{ slug: "grenoble", name: "Grenoble", region: "Auvergne-Rhône-Alpes", country: "France", population: 158000, currency: "EUR", currencySymbol: "€", latitude: 45.1885, longitude: 5.7245, neighborSlugs: ["echirolles", "chambery", "annecy"] },
|
|
3109
|
+
{ slug: "dijon", name: "Dijon", region: "Bourgogne-Franche-Comté", country: "France", population: 159000, currency: "EUR", currencySymbol: "€", latitude: 47.322, longitude: 5.0415, neighborSlugs: ["besancon", "chalon-sur-saone", "beaune"] },
|
|
3110
|
+
{ slug: "angers", name: "Angers", region: "Pays de la Loire", country: "France", population: 155000, currency: "EUR", currencySymbol: "€", latitude: 47.4784, longitude: -0.5632, neighborSlugs: ["saumur", "cholet", "nantes"] },
|
|
3111
|
+
{ slug: "villeurbanne", name: "Villeurbanne", region: "Auvergne-Rhône-Alpes", country: "France", population: 154000, currency: "EUR", currencySymbol: "€", latitude: 45.7667, longitude: 4.8833, neighborSlugs: ["lyon", "bron", "caluire-et-cuire"] },
|
|
3112
|
+
{ slug: "nimes", name: "Nîmes", region: "Occitanie", country: "France", population: 148000, currency: "EUR", currencySymbol: "€", latitude: 43.8367, longitude: 4.3601, neighborSlugs: ["montpellier", "avignon", "arles"] },
|
|
3113
|
+
{ slug: "aix-en-provence", name: "Aix-en-Provence", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 147000, currency: "EUR", currencySymbol: "€", latitude: 43.5297, longitude: 5.4474, neighborSlugs: ["marseille", "gardanne", "salon-de-provence"] },
|
|
3114
|
+
{ slug: "clermont-ferrand", name: "Clermont-Ferrand", region: "Auvergne-Rhône-Alpes", country: "France", population: 147000, currency: "EUR", currencySymbol: "€", latitude: 45.7772, longitude: 3.087, neighborSlugs: ["riom", "cournon-d-auvergne", "vichy"] },
|
|
3115
|
+
{ slug: "brest", name: "Brest", region: "Bretagne", country: "France", population: 139000, currency: "EUR", currencySymbol: "€", latitude: 48.3904, longitude: -4.4861, neighborSlugs: ["quimper", "morlaix", "rennes"] },
|
|
3116
|
+
{ slug: "tours", name: "Tours", region: "Centre-Val de Loire", country: "France", population: 137000, currency: "EUR", currencySymbol: "€", latitude: 47.3941, longitude: 0.6848, neighborSlugs: ["orleans", "blois", "angers"] },
|
|
3117
|
+
{ slug: "amiens", name: "Amiens", region: "Hauts-de-France", country: "France", population: 134000, currency: "EUR", currencySymbol: "€", latitude: 49.8941, longitude: 2.2958, neighborSlugs: ["beauvais", "compiegne", "lille"] },
|
|
3118
|
+
{ slug: "annecy", name: "Annecy", region: "Auvergne-Rhône-Alpes", country: "France", population: 131000, currency: "EUR", currencySymbol: "€", latitude: 45.8992, longitude: 6.1294, neighborSlugs: ["chambery", "geneve", "grenoble"] }
|
|
3119
|
+
],
|
|
3120
|
+
es: [
|
|
3121
|
+
{ slug: "madrid", name: "Madrid", region: "Comunidad de Madrid", country: "Spain", population: 3300000, currency: "EUR", currencySymbol: "€", latitude: 40.4168, longitude: -3.7038, neighborSlugs: ["alcala-de-henares", "mostoles", "getafe"] },
|
|
3122
|
+
{ slug: "barcelona", name: "Barcelona", region: "Catalunya", country: "Spain", population: 1636000, currency: "EUR", currencySymbol: "€", latitude: 41.3851, longitude: 2.1734, neighborSlugs: ["hospitalet-de-llobregat", "badalona", "terrassa"] },
|
|
3123
|
+
{ slug: "valencia", name: "Valencia", region: "Comunitat Valenciana", country: "Spain", population: 800000, currency: "EUR", currencySymbol: "€", latitude: 39.4699, longitude: -0.3763, neighborSlugs: ["alicante", "castellon-de-la-plana", "gandia"] },
|
|
3124
|
+
{ slug: "sevilla", name: "Sevilla", region: "Andalucía", country: "Spain", population: 688000, currency: "EUR", currencySymbol: "€", latitude: 37.3891, longitude: -5.9845, neighborSlugs: ["cordoba", "huelva", "cadiz"] },
|
|
3125
|
+
{ slug: "zaragoza", name: "Zaragoza", region: "Aragón", country: "Spain", population: 675000, currency: "EUR", currencySymbol: "€", latitude: 41.6488, longitude: -0.8891, neighborSlugs: ["huesca", "teruel", "madrid"] },
|
|
3126
|
+
{ slug: "malaga", name: "Málaga", region: "Andalucía", country: "Spain", population: 579000, currency: "EUR", currencySymbol: "€", latitude: 36.7213, longitude: -4.4214, neighborSlugs: ["marbella", "torremolinos", "granada"] },
|
|
3127
|
+
{ slug: "bilbao", name: "Bilbao", region: "País Vasco", country: "Spain", population: 346000, currency: "EUR", currencySymbol: "€", latitude: 43.263, longitude: -2.935, neighborSlugs: ["san-sebastian", "vitoria-gasteiz", "santander"] }
|
|
3128
|
+
],
|
|
3129
|
+
de: [
|
|
3130
|
+
{ slug: "berlin", name: "Berlin", region: "Berlin", country: "Germany", population: 3645000, currency: "EUR", currencySymbol: "€", latitude: 52.52, longitude: 13.405, neighborSlugs: ["potsdam", "cottbus", "brandenburg"] },
|
|
3131
|
+
{ slug: "hamburg", name: "Hamburg", region: "Hamburg", country: "Germany", population: 1841000, currency: "EUR", currencySymbol: "€", latitude: 53.5511, longitude: 9.9937, neighborSlugs: ["luebeck", "bremen", "kiel"] },
|
|
3132
|
+
{ slug: "muenchen", name: "München", region: "Bayern", country: "Germany", population: 1472000, currency: "EUR", currencySymbol: "€", latitude: 48.1351, longitude: 11.582, neighborSlugs: ["augsburg", "ingolstadt", "rosenheim"] },
|
|
3133
|
+
{ slug: "koeln", name: "Köln", region: "Nordrhein-Westfalen", country: "Germany", population: 1086000, currency: "EUR", currencySymbol: "€", latitude: 50.9375, longitude: 6.9603, neighborSlugs: ["duesseldorf", "bonn", "leverkusen"] },
|
|
3134
|
+
{ slug: "frankfurt", name: "Frankfurt am Main", region: "Hessen", country: "Germany", population: 753000, currency: "EUR", currencySymbol: "€", latitude: 50.1109, longitude: 8.6821, neighborSlugs: ["wiesbaden", "mainz", "darmstadt"] },
|
|
3135
|
+
{ slug: "stuttgart", name: "Stuttgart", region: "Baden-Württemberg", country: "Germany", population: 635000, currency: "EUR", currencySymbol: "€", latitude: 48.7758, longitude: 9.1829, neighborSlugs: ["esslingen", "ludwigsburg", "karlsruhe"] }
|
|
3136
|
+
],
|
|
3137
|
+
uk: [
|
|
3138
|
+
{ slug: "london", name: "London", region: "Greater London", country: "United Kingdom", population: 8982000, currency: "GBP", currencySymbol: "£", latitude: 51.5074, longitude: -0.1278, neighborSlugs: ["croydon", "watford", "reading"] },
|
|
3139
|
+
{ slug: "birmingham", name: "Birmingham", region: "West Midlands", country: "United Kingdom", population: 1149000, currency: "GBP", currencySymbol: "£", latitude: 52.4862, longitude: -1.8904, neighborSlugs: ["coventry", "wolverhampton", "solihull"] },
|
|
3140
|
+
{ slug: "manchester", name: "Manchester", region: "Greater Manchester", country: "United Kingdom", population: 553000, currency: "GBP", currencySymbol: "£", latitude: 53.4808, longitude: -2.2426, neighborSlugs: ["salford", "bolton", "stockport"] },
|
|
3141
|
+
{ slug: "edinburgh", name: "Edinburgh", region: "Scotland", country: "United Kingdom", population: 527000, currency: "GBP", currencySymbol: "£", latitude: 55.9533, longitude: -3.1883, neighborSlugs: ["glasgow", "livingston", "dundee"] }
|
|
3142
|
+
],
|
|
3143
|
+
us: [
|
|
3144
|
+
{ slug: "new-york", name: "New York", region: "NY", country: "United States", population: 8336000, currency: "USD", currencySymbol: "$", latitude: 40.7128, longitude: -74.006, neighborSlugs: ["brooklyn", "queens", "jersey-city"] },
|
|
3145
|
+
{ slug: "los-angeles", name: "Los Angeles", region: "CA", country: "United States", population: 3979000, currency: "USD", currencySymbol: "$", latitude: 34.0522, longitude: -118.2437, neighborSlugs: ["long-beach", "pasadena", "glendale"] },
|
|
3146
|
+
{ slug: "chicago", name: "Chicago", region: "IL", country: "United States", population: 2693000, currency: "USD", currencySymbol: "$", latitude: 41.8781, longitude: -87.6298, neighborSlugs: ["evanston", "naperville", "aurora"] },
|
|
3147
|
+
{ slug: "houston", name: "Houston", region: "TX", country: "United States", population: 2320000, currency: "USD", currencySymbol: "$", latitude: 29.7604, longitude: -95.3698, neighborSlugs: ["the-woodlands", "sugar-land", "katy"] },
|
|
3148
|
+
{ slug: "miami", name: "Miami", region: "FL", country: "United States", population: 442000, currency: "USD", currencySymbol: "$", latitude: 25.7617, longitude: -80.1918, neighborSlugs: ["fort-lauderdale", "miami-beach", "hialeah"] },
|
|
3149
|
+
{ slug: "san-francisco", name: "San Francisco", region: "CA", country: "United States", population: 873000, currency: "USD", currencySymbol: "$", latitude: 37.7749, longitude: -122.4194, neighborSlugs: ["oakland", "san-jose", "berkeley"] }
|
|
3150
|
+
],
|
|
3151
|
+
be: [
|
|
3152
|
+
{ slug: "bruxelles", name: "Bruxelles", region: "Bruxelles-Capitale", country: "Belgium", population: 1220000, currency: "EUR", currencySymbol: "€", latitude: 50.8503, longitude: 4.3517, neighborSlugs: ["anvers", "gand", "liege"] },
|
|
3153
|
+
{ slug: "anvers", name: "Anvers", region: "Flandre", country: "Belgium", population: 530000, currency: "EUR", currencySymbol: "€", latitude: 51.2194, longitude: 4.4025, neighborSlugs: ["bruxelles", "malines", "gand"] },
|
|
3154
|
+
{ slug: "liege", name: "Liège", region: "Wallonie", country: "Belgium", population: 197000, currency: "EUR", currencySymbol: "€", latitude: 50.6326, longitude: 5.5797, neighborSlugs: ["namur", "charleroi", "bruxelles"] }
|
|
3155
|
+
],
|
|
3156
|
+
ch: [
|
|
3157
|
+
{ slug: "zurich", name: "Zürich", region: "Zürich", country: "Switzerland", population: 434000, currency: "CHF", currencySymbol: "CHF", latitude: 47.3769, longitude: 8.5417, neighborSlugs: ["bale", "berne", "lucerne"] },
|
|
3158
|
+
{ slug: "geneve", name: "Genève", region: "Genève", country: "Switzerland", population: 203000, currency: "EUR", currencySymbol: "CHF", latitude: 46.2044, longitude: 6.1432, neighborSlugs: ["lausanne", "annecy", "nyon"] },
|
|
3159
|
+
{ slug: "lausanne", name: "Lausanne", region: "Vaud", country: "Switzerland", population: 140000, currency: "CHF", currencySymbol: "CHF", latitude: 46.5197, longitude: 6.6323, neighborSlugs: ["geneve", "montreux", "vevey"] }
|
|
3160
|
+
],
|
|
3161
|
+
it: [
|
|
3162
|
+
{ slug: "roma", name: "Roma", region: "Lazio", country: "Italy", population: 2873000, currency: "EUR", currencySymbol: "€", latitude: 41.9028, longitude: 12.4964, neighborSlugs: ["latina", "fiumicino", "tivoli"] },
|
|
3163
|
+
{ slug: "milano", name: "Milano", region: "Lombardia", country: "Italy", population: 1366000, currency: "EUR", currencySymbol: "€", latitude: 45.4642, longitude: 9.19, neighborSlugs: ["monza", "bergamo", "brescia"] },
|
|
3164
|
+
{ slug: "torino", name: "Torino", region: "Piemonte", country: "Italy", population: 870000, currency: "EUR", currencySymbol: "€", latitude: 45.0703, longitude: 7.6869, neighborSlugs: ["novara", "alessandria", "asti"] }
|
|
3165
|
+
],
|
|
3166
|
+
ca: [
|
|
3167
|
+
{ slug: "toronto", name: "Toronto", region: "ON", country: "Canada", population: 2794000, currency: "CAD", currencySymbol: "$", latitude: 43.6532, longitude: -79.3832, neighborSlugs: ["mississauga", "brampton", "markham"] },
|
|
3168
|
+
{ slug: "montreal", name: "Montréal", region: "QC", country: "Canada", population: 1762000, currency: "CAD", currencySymbol: "$", latitude: 45.5017, longitude: -73.5673, neighborSlugs: ["laval", "longueuil", "quebec"] },
|
|
3169
|
+
{ slug: "vancouver", name: "Vancouver", region: "BC", country: "Canada", population: 662000, currency: "CAD", currencySymbol: "$", latitude: 49.2827, longitude: -123.1207, neighborSlugs: ["burnaby", "richmond", "surrey"] }
|
|
3170
|
+
]
|
|
3171
|
+
};
|
|
3172
|
+
function resolveBuiltInLocations(territories) {
|
|
3173
|
+
if (!territories) {
|
|
3174
|
+
return [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
3175
|
+
}
|
|
3176
|
+
const requested = (Array.isArray(territories) ? territories : [territories]).map((t) => t.toLowerCase().trim());
|
|
3177
|
+
const results = [];
|
|
3178
|
+
const addedSlugs = new Set;
|
|
3179
|
+
const isAllOrEurope = requested.some((t) => ["europe", "international", "all", "global", "world"].includes(t));
|
|
3180
|
+
if (isAllOrEurope) {
|
|
3181
|
+
for (const list of Object.values(BUILT_IN_LOCATIONS_DATABASE)) {
|
|
3182
|
+
for (const loc of list) {
|
|
3183
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
3184
|
+
addedSlugs.add(loc.slug);
|
|
3185
|
+
results.push(loc);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
return results;
|
|
3190
|
+
}
|
|
3191
|
+
const countryAliasMap = {
|
|
3192
|
+
france: "fr",
|
|
3193
|
+
fr: "fr",
|
|
3194
|
+
spain: "es",
|
|
3195
|
+
espagne: "es",
|
|
3196
|
+
es: "es",
|
|
3197
|
+
germany: "de",
|
|
3198
|
+
allemagne: "de",
|
|
3199
|
+
de: "de",
|
|
3200
|
+
uk: "uk",
|
|
3201
|
+
gb: "uk",
|
|
3202
|
+
"united kingdom": "uk",
|
|
3203
|
+
angleterre: "uk",
|
|
3204
|
+
us: "us",
|
|
3205
|
+
usa: "us",
|
|
3206
|
+
"united states": "us",
|
|
3207
|
+
etatsunis: "us",
|
|
3208
|
+
belgium: "be",
|
|
3209
|
+
belgique: "be",
|
|
3210
|
+
be: "be",
|
|
3211
|
+
switzerland: "ch",
|
|
3212
|
+
suisse: "ch",
|
|
3213
|
+
ch: "ch",
|
|
3214
|
+
italy: "it",
|
|
3215
|
+
italie: "it",
|
|
3216
|
+
it: "it",
|
|
3217
|
+
canada: "ca",
|
|
3218
|
+
ca: "ca"
|
|
3219
|
+
};
|
|
3220
|
+
for (const req of requested) {
|
|
3221
|
+
const code = countryAliasMap[req.replace(/[^a-z0-9]/g, "")];
|
|
3222
|
+
if (code && BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
3223
|
+
for (const loc of BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
3224
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
3225
|
+
addedSlugs.add(loc.slug);
|
|
3226
|
+
results.push(loc);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3090
3234
|
// src/matrix-engine.ts
|
|
3091
3235
|
class PseoMatrixEngine {
|
|
3092
3236
|
legalEngine;
|
|
@@ -3169,7 +3313,7 @@ class PseoMatrixEngine {
|
|
|
3169
3313
|
const countryCode = cleanSeoSlug(loc.country, { language: lang });
|
|
3170
3314
|
const citySlug = cleanSeoSlug(loc.slug, { language: lang });
|
|
3171
3315
|
const serviceSlug = cleanSeoSlug(s.slug, { language: lang });
|
|
3172
|
-
const urlPath = `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
3316
|
+
const urlPath = options.cleanDirectRoutes !== false ? `/${serviceSlug}/${citySlug}` : `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
3173
3317
|
const fullUrl = `${cleanDomain}${urlPath}`;
|
|
3174
3318
|
const isIndexed = (loc.population ?? 20000) >= minPop;
|
|
3175
3319
|
const robots = isIndexed ? "index, follow" : "noindex, follow";
|
|
@@ -3564,8 +3708,9 @@ class PseoMatrixEngine {
|
|
|
3564
3708
|
}
|
|
3565
3709
|
generateAllMatrices(domain, data, options) {
|
|
3566
3710
|
const allPages = [];
|
|
3567
|
-
if (data.services
|
|
3568
|
-
|
|
3711
|
+
if (data.services) {
|
|
3712
|
+
const locs = data.locations && data.locations.length > 0 ? data.locations : resolveBuiltInLocations(options.countries || options.territories || options.language || "fr");
|
|
3713
|
+
allPages.push(...this.generateLocalGeoMatrix(domain, data.services, locs, options));
|
|
3569
3714
|
}
|
|
3570
3715
|
if (data.competitors) {
|
|
3571
3716
|
allPages.push(...this.generateVsMatrix(domain, data.competitors, options));
|
|
@@ -3592,6 +3737,40 @@ class PseoMatrixEngine {
|
|
|
3592
3737
|
}
|
|
3593
3738
|
return allPages;
|
|
3594
3739
|
}
|
|
3740
|
+
resolvePage(slugOrPath, domain, data, options) {
|
|
3741
|
+
const path = typeof slugOrPath === "string" ? slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}` : `/${slugOrPath.join("/")}`;
|
|
3742
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
3743
|
+
return pages.find((p) => p.urlPath === path);
|
|
3744
|
+
}
|
|
3745
|
+
generateSitemapXml(domain, data, options) {
|
|
3746
|
+
const pages = this.generateAllMatrices(domain, data, options).filter((p) => p.robots.includes("index"));
|
|
3747
|
+
const now = new Date().toISOString();
|
|
3748
|
+
const urls = pages.map((p) => ` <url>
|
|
3749
|
+
<loc>${p.canonicalUrl}</loc>
|
|
3750
|
+
<lastmod>${now}</lastmod>
|
|
3751
|
+
<changefreq>weekly</changefreq>
|
|
3752
|
+
<priority>0.8</priority>
|
|
3753
|
+
</url>`).join(`
|
|
3754
|
+
`);
|
|
3755
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3756
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3757
|
+
${urls}
|
|
3758
|
+
</urlset>`;
|
|
3759
|
+
}
|
|
3760
|
+
generateLlmsTxt(domain, data, options) {
|
|
3761
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
3762
|
+
const lines = [
|
|
3763
|
+
`# ${options.brandName} — AI Knowledge Graph & Page Hub`,
|
|
3764
|
+
`> Complete index of services, tools, integrations, and local solution hubs.`,
|
|
3765
|
+
"",
|
|
3766
|
+
`## Indexed Programmatic Hubs (${pages.length} Pages)`
|
|
3767
|
+
];
|
|
3768
|
+
for (const p of pages) {
|
|
3769
|
+
lines.push(`- [${p.h1}](${p.canonicalUrl}): ${p.description}`);
|
|
3770
|
+
}
|
|
3771
|
+
return lines.join(`
|
|
3772
|
+
`);
|
|
3773
|
+
}
|
|
3595
3774
|
}
|
|
3596
3775
|
// src/brand-icons.ts
|
|
3597
3776
|
var BRAND_ICONS = {
|
|
@@ -3881,7 +4060,21 @@ When discovering product modules, features, or services to build the SEO matrice
|
|
|
3881
4060
|
|
|
3882
4061
|
---
|
|
3883
4062
|
|
|
3884
|
-
### \
|
|
4063
|
+
### \uD83D\uDCCF 3. THE GOLDEN RULES OF URL ARCHITECTURE (MAX 3-4 SEGMENTS, ZERO PARASITE WORDS)
|
|
4064
|
+
|
|
4065
|
+
- **Rule A (2 to 3 URL Segments Ideal, 4 Maximum):**
|
|
4066
|
+
- ✅ **2 Segments (Top SEO Performance):** \`site.com/{service}/{city}\` (e.g. \`/autopost-facebook/lyon\`, \`/crm/paris\`).
|
|
4067
|
+
- ✅ **3 Segments (B2B Persona/Hub):** \`site.com/for/{target}/{city}\` or \`site.com/{locale}/{service}/{city}\`.
|
|
4068
|
+
- ❌ **Never 5 to 6 Segments:** Avoid \`/solutions/ai/autopost/facebook/fr/lyon\`.
|
|
4069
|
+
- **Rule B (Eliminate All Parasite Noise Words):**
|
|
4070
|
+
- Banish generic wrapper words like \`/solutions/\`, \`/pages/\`, \`/items/\`. Go direct to user search intent: What you do + Platform + Location.
|
|
4071
|
+
- **Rule C (The SDK Does 100% of the Heavy Lifting):**
|
|
4072
|
+
- The developer/AI only registers 3-4 product features and target cities in \`lib/seo.ts\`.
|
|
4073
|
+
- The SDK automatically resolves routes in memory via \`matrixEngine.resolvePage(params.slug)\`, generates complete Schema.org graphs (LocalBusiness, AggregateRating, AggregateOffer), creates AEO summaries, builds FAQ accordions, and manages mesh linking in < 0.05ms.
|
|
4074
|
+
|
|
4075
|
+
---
|
|
4076
|
+
|
|
4077
|
+
### \uD83C\uDFC6 4. THE ARCHITECTURAL PLAYBOOK OF SAAS TITANS (7M+ PAGES WITH 0 DB BOTTLENECKS)
|
|
3885
4078
|
|
|
3886
4079
|
How category leaders generate millions of high-converting pages without server saturation:
|
|
3887
4080
|
|
|
@@ -3893,18 +4086,18 @@ How category leaders generate millions of high-converting pages without server s
|
|
|
3893
4086
|
|
|
3894
4087
|
2. **The 4 Pillars of Zero-Latency (< 0.05ms) Resolution:**
|
|
3895
4088
|
- **0 Bytes in Database:** Never generate physical static files or query heavy databases per request. Pages are resolved mathematically in RAM upon arrival.
|
|
3896
|
-
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\`
|
|
3897
|
-
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge
|
|
4089
|
+
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\` handles all matrices through one lightweight layout.
|
|
4090
|
+
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge on first request.
|
|
3898
4091
|
- **Automatic Siloing & Mesh Linking:**
|
|
3899
4092
|
- Links to 3 neighboring geographic cities for the same service.
|
|
3900
4093
|
- Links to 4 related professions/industries in the same target category.
|
|
3901
4094
|
- Complete Schema.org BreadcrumbList navigation.
|
|
3902
|
-
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index
|
|
4095
|
+
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index.
|
|
3903
4096
|
- **Dynamic \`/llms.txt\` Feed:** High-density Markdown stream enabling direct indexing by ChatGPT Search & Perplexity.
|
|
3904
4097
|
|
|
3905
4098
|
---
|
|
3906
4099
|
|
|
3907
|
-
### \uD83D\uDEE0️
|
|
4100
|
+
### \uD83D\uDEE0️ 5. AI AGENT IMPLEMENTATION PROTOCOL (STEP-BY-STEP)
|
|
3908
4101
|
|
|
3909
4102
|
When setting up programmatic SEO in any codebase, the AI Agent MUST generate these exact 4 files:
|
|
3910
4103
|
|
|
@@ -3916,27 +4109,30 @@ export const matrixEngine = new PseoMatrixEngine();
|
|
|
3916
4109
|
|
|
3917
4110
|
export const SEO_CONFIG = {
|
|
3918
4111
|
brandName: "Acme",
|
|
3919
|
-
language: "
|
|
4112
|
+
language: "fr",
|
|
4113
|
+
countries: ["france", "belgique", "suisse"], // Or "europe" / "international". The SDK automatically provides verified cities!
|
|
4114
|
+
cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.)
|
|
3920
4115
|
minPopulationToIndex: 15_000,
|
|
3921
4116
|
};
|
|
3922
4117
|
|
|
3923
4118
|
// Discovered EXCLUSIVELY from Frontend UI components
|
|
4119
|
+
// NOTE: DO NOT list cities manually! The SDK auto-provisions them based on your target countries/scope.
|
|
3924
4120
|
export const PSEO_DATASET = {
|
|
3925
4121
|
services: [
|
|
3926
4122
|
// 1 per major frontend feature branch
|
|
3927
|
-
{ slug: "
|
|
4123
|
+
{ slug: "autopost-facebook", name: "Autopost Facebook & Instagram", category: "Social Media", keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"], description: "Automated social media posting engine" },
|
|
3928
4124
|
],
|
|
3929
4125
|
targets: [
|
|
3930
|
-
{ slug: "
|
|
4126
|
+
{ slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] },
|
|
3931
4127
|
],
|
|
3932
4128
|
integrations: [
|
|
3933
|
-
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["
|
|
4129
|
+
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] },
|
|
3934
4130
|
],
|
|
3935
4131
|
templates: [
|
|
3936
|
-
{ slug: "
|
|
4132
|
+
{ slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Instant setup"] },
|
|
3937
4133
|
],
|
|
3938
4134
|
glossaryTerms: [
|
|
3939
|
-
{ slug: "
|
|
4135
|
+
{ slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" },
|
|
3940
4136
|
],
|
|
3941
4137
|
};
|
|
3942
4138
|
\`\`\`
|
|
@@ -3949,19 +4145,15 @@ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
|
|
|
3949
4145
|
import { renderBrandIconSvg } from "@lynxflow/seo-engine";
|
|
3950
4146
|
|
|
3951
4147
|
export async function generateMetadata({ params }: { params: { slug: string[] } }) {
|
|
3952
|
-
const path = "/" + params.slug.join("/");
|
|
3953
4148
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
3954
|
-
const
|
|
3955
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
4149
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
3956
4150
|
if (!page) return {};
|
|
3957
4151
|
return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
|
|
3958
4152
|
}
|
|
3959
4153
|
|
|
3960
4154
|
export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
|
|
3961
|
-
const path = "/" + params.slug.join("/");
|
|
3962
4155
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
3963
|
-
const
|
|
3964
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
4156
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
3965
4157
|
if (!page) notFound();
|
|
3966
4158
|
|
|
3967
4159
|
// Automatic Brand Icon Detection
|
|
@@ -6251,6 +6443,7 @@ var LynxSeo = {
|
|
|
6251
6443
|
var src_default = LynxSeo;
|
|
6252
6444
|
export {
|
|
6253
6445
|
validateSeoSlug,
|
|
6446
|
+
resolveBuiltInLocations,
|
|
6254
6447
|
resolveBrandIcon,
|
|
6255
6448
|
renderBrandIconSvg,
|
|
6256
6449
|
getSeoAgentPrompt,
|
|
@@ -6304,6 +6497,7 @@ export {
|
|
|
6304
6497
|
CopywritingFrameworksMaster,
|
|
6305
6498
|
BrandDnaCalendarEngine,
|
|
6306
6499
|
BacklinksClient,
|
|
6500
|
+
BUILT_IN_LOCATIONS_DATABASE,
|
|
6307
6501
|
BRAND_ICONS,
|
|
6308
6502
|
ApiKeyGuardian,
|
|
6309
6503
|
AiCopilotClient,
|
package/dist/matrix-engine.d.ts
CHANGED
|
@@ -127,6 +127,9 @@ export interface GeneratedPageMeta {
|
|
|
127
127
|
export interface MatrixOptions {
|
|
128
128
|
brandName: string;
|
|
129
129
|
language?: string;
|
|
130
|
+
countries?: string[] | string;
|
|
131
|
+
territories?: string[] | string;
|
|
132
|
+
cleanDirectRoutes?: boolean;
|
|
130
133
|
minPopulationToIndex?: number;
|
|
131
134
|
defaultCurrency?: string;
|
|
132
135
|
defaultCurrencySymbol?: string;
|
|
@@ -198,4 +201,27 @@ export declare class PseoMatrixEngine {
|
|
|
198
201
|
glossaryTerms?: PseoGlossaryTerm[];
|
|
199
202
|
calculators?: PseoCalculator[];
|
|
200
203
|
}, options: MatrixOptions): GeneratedPageMeta[];
|
|
204
|
+
/**
|
|
205
|
+
* Resolves a single page route in RAM in < 0.05ms.
|
|
206
|
+
* Matches string path or Next.js slug array.
|
|
207
|
+
*/
|
|
208
|
+
resolvePage(slugOrPath: string | string[], domain: string, data: {
|
|
209
|
+
services?: PseoService[];
|
|
210
|
+
locations?: PseoLocation[];
|
|
211
|
+
competitors?: PseoCompetitor[];
|
|
212
|
+
targets?: PseoTarget[];
|
|
213
|
+
integrations?: PseoIntegration[];
|
|
214
|
+
useCases?: PseoUseCase[];
|
|
215
|
+
templates?: PseoTemplate[];
|
|
216
|
+
glossaryTerms?: PseoGlossaryTerm[];
|
|
217
|
+
calculators?: PseoCalculator[];
|
|
218
|
+
}, options: MatrixOptions): GeneratedPageMeta | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* Generates a fully formatted XML Sitemap string.
|
|
221
|
+
*/
|
|
222
|
+
generateSitemapXml(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
|
|
223
|
+
/**
|
|
224
|
+
* Generates an official /llms.txt Markdown directory for AI search bots.
|
|
225
|
+
*/
|
|
226
|
+
generateLlmsTxt(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
|
|
201
227
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🌍 Built-in Global Geographic & Demographic Database
|
|
3
|
+
* Automatically provisions verified cities, populations, currencies, and coordinates
|
|
4
|
+
* across Europe, North America, and international markets without requiring the user to list cities manually.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PseoLocation } from "./matrix-engine";
|
|
8
|
+
|
|
9
|
+
export const BUILT_IN_LOCATIONS_DATABASE: Record<string, PseoLocation[]> = {
|
|
10
|
+
fr: [
|
|
11
|
+
{ slug: "paris", name: "Paris", region: "Île-de-France", country: "France", population: 2_161_000, currency: "EUR", currencySymbol: "€", latitude: 48.8566, longitude: 2.3522, neighborSlugs: ["boulogne-billancourt", "saint-denis", "montreuil"] },
|
|
12
|
+
{ slug: "marseille", name: "Marseille", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 873_000, currency: "EUR", currencySymbol: "€", latitude: 43.2965, longitude: 5.3698, neighborSlugs: ["aix-en-provence", "aubagne", "toulon"] },
|
|
13
|
+
{ slug: "lyon", name: "Lyon", region: "Auvergne-Rhône-Alpes", country: "France", population: 522_000, currency: "EUR", currencySymbol: "€", latitude: 45.764, longitude: 4.8357, neighborSlugs: ["villeurbanne", "venissieux", "saint-etienne"] },
|
|
14
|
+
{ slug: "toulouse", name: "Toulouse", region: "Occitanie", country: "France", population: 498_000, currency: "EUR", currencySymbol: "€", latitude: 43.6047, longitude: 1.4442, neighborSlugs: ["montauban", "albi", "colomiers"] },
|
|
15
|
+
{ slug: "nice", name: "Nice", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 343_000, currency: "EUR", currencySymbol: "€", latitude: 43.7102, longitude: 7.262, neighborSlugs: ["cannes", "antibes", "monaco"] },
|
|
16
|
+
{ slug: "nantes", name: "Nantes", region: "Pays de la Loire", country: "France", population: 320_000, currency: "EUR", currencySymbol: "€", latitude: 47.2184, longitude: -1.5536, neighborSlugs: ["saint-nazaire", "angers", "rennes"] },
|
|
17
|
+
{ slug: "montpellier", name: "Montpellier", region: "Occitanie", country: "France", population: 299_000, currency: "EUR", currencySymbol: "€", latitude: 43.6108, longitude: 3.8767, neighborSlugs: ["nimes", "beziers", "sete"] },
|
|
18
|
+
{ slug: "strasbourg", name: "Strasbourg", region: "Grand Est", country: "France", population: 290_000, currency: "EUR", currencySymbol: "€", latitude: 48.5734, longitude: 7.7521, neighborSlugs: ["mulhouse", "colmar", "metz"] },
|
|
19
|
+
{ slug: "bordeaux", name: "Bordeaux", region: "Nouvelle-Aquitaine", country: "France", population: 260_000, currency: "EUR", currencySymbol: "€", latitude: 44.8378, longitude: -0.5792, neighborSlugs: ["merignac", "pessac", "arcachon"] },
|
|
20
|
+
{ slug: "lille", name: "Lille", region: "Hauts-de-France", country: "France", population: 236_000, currency: "EUR", currencySymbol: "€", latitude: 50.6292, longitude: 3.0573, neighborSlugs: ["roubaix", "tourcoing", "villeneuve-d-ascq"] },
|
|
21
|
+
{ slug: "rennes", name: "Rennes", region: "Bretagne", country: "France", population: 222_000, currency: "EUR", currencySymbol: "€", latitude: 48.1173, longitude: -1.6778, neighborSlugs: ["saint-malo", "brest", "nantes"] },
|
|
22
|
+
{ slug: "toulon", name: "Toulon", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 179_000, currency: "EUR", currencySymbol: "€", latitude: 43.1242, longitude: 5.928, neighborSlugs: ["hyeres", "la-seyne-sur-mer", "marseille"] },
|
|
23
|
+
{ slug: "reims", name: "Reims", region: "Grand Est", country: "France", population: 179_000, currency: "EUR", currencySymbol: "€", latitude: 49.2583, longitude: 4.0317, neighborSlugs: ["chalons-en-champagne", "troyes", "metz"] },
|
|
24
|
+
{ slug: "saint-etienne", name: "Saint-Étienne", region: "Auvergne-Rhône-Alpes", country: "France", population: 174_000, currency: "EUR", currencySymbol: "€", latitude: 45.4397, longitude: 4.3872, neighborSlugs: ["lyon", "roanne", "saint-chamond"] },
|
|
25
|
+
{ slug: "le-havre", name: "Le Havre", region: "Normandie", country: "France", population: 166_000, currency: "EUR", currencySymbol: "€", latitude: 49.4944, longitude: 0.1079, neighborSlugs: ["rouen", "caen", "honfleur"] },
|
|
26
|
+
{ slug: "grenoble", name: "Grenoble", region: "Auvergne-Rhône-Alpes", country: "France", population: 158_000, currency: "EUR", currencySymbol: "€", latitude: 45.1885, longitude: 5.7245, neighborSlugs: ["echirolles", "chambery", "annecy"] },
|
|
27
|
+
{ slug: "dijon", name: "Dijon", region: "Bourgogne-Franche-Comté", country: "France", population: 159_000, currency: "EUR", currencySymbol: "€", latitude: 47.322, longitude: 5.0415, neighborSlugs: ["besancon", "chalon-sur-saone", "beaune"] },
|
|
28
|
+
{ slug: "angers", name: "Angers", region: "Pays de la Loire", country: "France", population: 155_000, currency: "EUR", currencySymbol: "€", latitude: 47.4784, longitude: -0.5632, neighborSlugs: ["saumur", "cholet", "nantes"] },
|
|
29
|
+
{ slug: "villeurbanne", name: "Villeurbanne", region: "Auvergne-Rhône-Alpes", country: "France", population: 154_000, currency: "EUR", currencySymbol: "€", latitude: 45.7667, longitude: 4.8833, neighborSlugs: ["lyon", "bron", "caluire-et-cuire"] },
|
|
30
|
+
{ slug: "nimes", name: "Nîmes", region: "Occitanie", country: "France", population: 148_000, currency: "EUR", currencySymbol: "€", latitude: 43.8367, longitude: 4.3601, neighborSlugs: ["montpellier", "avignon", "arles"] },
|
|
31
|
+
{ slug: "aix-en-provence", name: "Aix-en-Provence", region: "Provence-Alpes-Côte d'Azur", country: "France", population: 147_000, currency: "EUR", currencySymbol: "€", latitude: 43.5297, longitude: 5.4474, neighborSlugs: ["marseille", "gardanne", "salon-de-provence"] },
|
|
32
|
+
{ slug: "clermont-ferrand", name: "Clermont-Ferrand", region: "Auvergne-Rhône-Alpes", country: "France", population: 147_000, currency: "EUR", currencySymbol: "€", latitude: 45.7772, longitude: 3.087, neighborSlugs: ["riom", "cournon-d-auvergne", "vichy"] },
|
|
33
|
+
{ slug: "brest", name: "Brest", region: "Bretagne", country: "France", population: 139_000, currency: "EUR", currencySymbol: "€", latitude: 48.3904, longitude: -4.4861, neighborSlugs: ["quimper", "morlaix", "rennes"] },
|
|
34
|
+
{ slug: "tours", name: "Tours", region: "Centre-Val de Loire", country: "France", population: 137_000, currency: "EUR", currencySymbol: "€", latitude: 47.3941, longitude: 0.6848, neighborSlugs: ["orleans", "blois", "angers"] },
|
|
35
|
+
{ slug: "amiens", name: "Amiens", region: "Hauts-de-France", country: "France", population: 134_000, currency: "EUR", currencySymbol: "€", latitude: 49.8941, longitude: 2.2958, neighborSlugs: ["beauvais", "compiegne", "lille"] },
|
|
36
|
+
{ slug: "annecy", name: "Annecy", region: "Auvergne-Rhône-Alpes", country: "France", population: 131_000, currency: "EUR", currencySymbol: "€", latitude: 45.8992, longitude: 6.1294, neighborSlugs: ["chambery", "geneve", "grenoble"] },
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
es: [
|
|
40
|
+
{ slug: "madrid", name: "Madrid", region: "Comunidad de Madrid", country: "Spain", population: 3_300_000, currency: "EUR", currencySymbol: "€", latitude: 40.4168, longitude: -3.7038, neighborSlugs: ["alcala-de-henares", "mostoles", "getafe"] },
|
|
41
|
+
{ slug: "barcelona", name: "Barcelona", region: "Catalunya", country: "Spain", population: 1_636_000, currency: "EUR", currencySymbol: "€", latitude: 41.3851, longitude: 2.1734, neighborSlugs: ["hospitalet-de-llobregat", "badalona", "terrassa"] },
|
|
42
|
+
{ slug: "valencia", name: "Valencia", region: "Comunitat Valenciana", country: "Spain", population: 800_000, currency: "EUR", currencySymbol: "€", latitude: 39.4699, longitude: -0.3763, neighborSlugs: ["alicante", "castellon-de-la-plana", "gandia"] },
|
|
43
|
+
{ slug: "sevilla", name: "Sevilla", region: "Andalucía", country: "Spain", population: 688_000, currency: "EUR", currencySymbol: "€", latitude: 37.3891, longitude: -5.9845, neighborSlugs: ["cordoba", "huelva", "cadiz"] },
|
|
44
|
+
{ slug: "zaragoza", name: "Zaragoza", region: "Aragón", country: "Spain", population: 675_000, currency: "EUR", currencySymbol: "€", latitude: 41.6488, longitude: -0.8891, neighborSlugs: ["huesca", "teruel", "madrid"] },
|
|
45
|
+
{ slug: "malaga", name: "Málaga", region: "Andalucía", country: "Spain", population: 579_000, currency: "EUR", currencySymbol: "€", latitude: 36.7213, longitude: -4.4214, neighborSlugs: ["marbella", "torremolinos", "granada"] },
|
|
46
|
+
{ slug: "bilbao", name: "Bilbao", region: "País Vasco", country: "Spain", population: 346_000, currency: "EUR", currencySymbol: "€", latitude: 43.263, longitude: -2.935, neighborSlugs: ["san-sebastian", "vitoria-gasteiz", "santander"] },
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
de: [
|
|
50
|
+
{ slug: "berlin", name: "Berlin", region: "Berlin", country: "Germany", population: 3_645_000, currency: "EUR", currencySymbol: "€", latitude: 52.52, longitude: 13.405, neighborSlugs: ["potsdam", "cottbus", "brandenburg"] },
|
|
51
|
+
{ slug: "hamburg", name: "Hamburg", region: "Hamburg", country: "Germany", population: 1_841_000, currency: "EUR", currencySymbol: "€", latitude: 53.5511, longitude: 9.9937, neighborSlugs: ["luebeck", "bremen", "kiel"] },
|
|
52
|
+
{ slug: "muenchen", name: "München", region: "Bayern", country: "Germany", population: 1_472_000, currency: "EUR", currencySymbol: "€", latitude: 48.1351, longitude: 11.582, neighborSlugs: ["augsburg", "ingolstadt", "rosenheim"] },
|
|
53
|
+
{ slug: "koeln", name: "Köln", region: "Nordrhein-Westfalen", country: "Germany", population: 1_086_000, currency: "EUR", currencySymbol: "€", latitude: 50.9375, longitude: 6.9603, neighborSlugs: ["duesseldorf", "bonn", "leverkusen"] },
|
|
54
|
+
{ slug: "frankfurt", name: "Frankfurt am Main", region: "Hessen", country: "Germany", population: 753_000, currency: "EUR", currencySymbol: "€", latitude: 50.1109, longitude: 8.6821, neighborSlugs: ["wiesbaden", "mainz", "darmstadt"] },
|
|
55
|
+
{ slug: "stuttgart", name: "Stuttgart", region: "Baden-Württemberg", country: "Germany", population: 635_000, currency: "EUR", currencySymbol: "€", latitude: 48.7758, longitude: 9.1829, neighborSlugs: ["esslingen", "ludwigsburg", "karlsruhe"] },
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
uk: [
|
|
59
|
+
{ slug: "london", name: "London", region: "Greater London", country: "United Kingdom", population: 8_982_000, currency: "GBP", currencySymbol: "£", latitude: 51.5074, longitude: -0.1278, neighborSlugs: ["croydon", "watford", "reading"] },
|
|
60
|
+
{ slug: "birmingham", name: "Birmingham", region: "West Midlands", country: "United Kingdom", population: 1_149_000, currency: "GBP", currencySymbol: "£", latitude: 52.4862, longitude: -1.8904, neighborSlugs: ["coventry", "wolverhampton", "solihull"] },
|
|
61
|
+
{ slug: "manchester", name: "Manchester", region: "Greater Manchester", country: "United Kingdom", population: 553_000, currency: "GBP", currencySymbol: "£", latitude: 53.4808, longitude: -2.2426, neighborSlugs: ["salford", "bolton", "stockport"] },
|
|
62
|
+
{ slug: "edinburgh", name: "Edinburgh", region: "Scotland", country: "United Kingdom", population: 527_000, currency: "GBP", currencySymbol: "£", latitude: 55.9533, longitude: -3.1883, neighborSlugs: ["glasgow", "livingston", "dundee"] },
|
|
63
|
+
],
|
|
64
|
+
|
|
65
|
+
us: [
|
|
66
|
+
{ slug: "new-york", name: "New York", region: "NY", country: "United States", population: 8_336_000, currency: "USD", currencySymbol: "$", latitude: 40.7128, longitude: -74.006, neighborSlugs: ["brooklyn", "queens", "jersey-city"] },
|
|
67
|
+
{ slug: "los-angeles", name: "Los Angeles", region: "CA", country: "United States", population: 3_979_000, currency: "USD", currencySymbol: "$", latitude: 34.0522, longitude: -118.2437, neighborSlugs: ["long-beach", "pasadena", "glendale"] },
|
|
68
|
+
{ slug: "chicago", name: "Chicago", region: "IL", country: "United States", population: 2_693_000, currency: "USD", currencySymbol: "$", latitude: 41.8781, longitude: -87.6298, neighborSlugs: ["evanston", "naperville", "aurora"] },
|
|
69
|
+
{ slug: "houston", name: "Houston", region: "TX", country: "United States", population: 2_320_000, currency: "USD", currencySymbol: "$", latitude: 29.7604, longitude: -95.3698, neighborSlugs: ["the-woodlands", "sugar-land", "katy"] },
|
|
70
|
+
{ slug: "miami", name: "Miami", region: "FL", country: "United States", population: 442_000, currency: "USD", currencySymbol: "$", latitude: 25.7617, longitude: -80.1918, neighborSlugs: ["fort-lauderdale", "miami-beach", "hialeah"] },
|
|
71
|
+
{ slug: "san-francisco", name: "San Francisco", region: "CA", country: "United States", population: 873_000, currency: "USD", currencySymbol: "$", latitude: 37.7749, longitude: -122.4194, neighborSlugs: ["oakland", "san-jose", "berkeley"] },
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
be: [
|
|
75
|
+
{ slug: "bruxelles", name: "Bruxelles", region: "Bruxelles-Capitale", country: "Belgium", population: 1_220_000, currency: "EUR", currencySymbol: "€", latitude: 50.8503, longitude: 4.3517, neighborSlugs: ["anvers", "gand", "liege"] },
|
|
76
|
+
{ slug: "anvers", name: "Anvers", region: "Flandre", country: "Belgium", population: 530_000, currency: "EUR", currencySymbol: "€", latitude: 51.2194, longitude: 4.4025, neighborSlugs: ["bruxelles", "malines", "gand"] },
|
|
77
|
+
{ slug: "liege", name: "Liège", region: "Wallonie", country: "Belgium", population: 197_000, currency: "EUR", currencySymbol: "€", latitude: 50.6326, longitude: 5.5797, neighborSlugs: ["namur", "charleroi", "bruxelles"] },
|
|
78
|
+
],
|
|
79
|
+
|
|
80
|
+
ch: [
|
|
81
|
+
{ slug: "zurich", name: "Zürich", region: "Zürich", country: "Switzerland", population: 434_000, currency: "CHF", currencySymbol: "CHF", latitude: 47.3769, longitude: 8.5417, neighborSlugs: ["bale", "berne", "lucerne"] },
|
|
82
|
+
{ slug: "geneve", name: "Genève", region: "Genève", country: "Switzerland", population: 203_000, currency: "EUR", currencySymbol: "CHF", latitude: 46.2044, longitude: 6.1432, neighborSlugs: ["lausanne", "annecy", "nyon"] },
|
|
83
|
+
{ slug: "lausanne", name: "Lausanne", region: "Vaud", country: "Switzerland", population: 140_000, currency: "CHF", currencySymbol: "CHF", latitude: 46.5197, longitude: 6.6323, neighborSlugs: ["geneve", "montreux", "vevey"] },
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
it: [
|
|
87
|
+
{ slug: "roma", name: "Roma", region: "Lazio", country: "Italy", population: 2_873_000, currency: "EUR", currencySymbol: "€", latitude: 41.9028, longitude: 12.4964, neighborSlugs: ["latina", "fiumicino", "tivoli"] },
|
|
88
|
+
{ slug: "milano", name: "Milano", region: "Lombardia", country: "Italy", population: 1_366_000, currency: "EUR", currencySymbol: "€", latitude: 45.4642, longitude: 9.19, neighborSlugs: ["monza", "bergamo", "brescia"] },
|
|
89
|
+
{ slug: "torino", name: "Torino", region: "Piemonte", country: "Italy", population: 870_000, currency: "EUR", currencySymbol: "€", latitude: 45.0703, longitude: 7.6869, neighborSlugs: ["novara", "alessandria", "asti"] },
|
|
90
|
+
],
|
|
91
|
+
|
|
92
|
+
ca: [
|
|
93
|
+
{ slug: "toronto", name: "Toronto", region: "ON", country: "Canada", population: 2_794_000, currency: "CAD", currencySymbol: "$", latitude: 43.6532, longitude: -79.3832, neighborSlugs: ["mississauga", "brampton", "markham"] },
|
|
94
|
+
{ slug: "montreal", name: "Montréal", region: "QC", country: "Canada", population: 1_762_000, currency: "CAD", currencySymbol: "$", latitude: 45.5017, longitude: -73.5673, neighborSlugs: ["laval", "longueuil", "quebec"] },
|
|
95
|
+
{ slug: "vancouver", name: "Vancouver", region: "BC", country: "Canada", population: 662_000, currency: "CAD", currencySymbol: "$", latitude: 49.2827, longitude: -123.1207, neighborSlugs: ["burnaby", "richmond", "surrey"] },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves built-in locations by country code or territory keyword.
|
|
101
|
+
* Supports: "fr", "france", "es", "spain", "de", "germany", "uk", "us", "be", "ch", "it", "ca", "europe", "international", "all"
|
|
102
|
+
*/
|
|
103
|
+
export function resolveBuiltInLocations(territories?: string[] | string): PseoLocation[] {
|
|
104
|
+
if (!territories) {
|
|
105
|
+
// Default: France + Belgium + Switzerland if unspecified
|
|
106
|
+
return [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const requested = (Array.isArray(territories) ? territories : [territories]).map((t) => t.toLowerCase().trim());
|
|
110
|
+
const results: PseoLocation[] = [];
|
|
111
|
+
const addedSlugs = new Set<string>();
|
|
112
|
+
|
|
113
|
+
const isAllOrEurope = requested.some((t) => ["europe", "international", "all", "global", "world"].includes(t));
|
|
114
|
+
|
|
115
|
+
if (isAllOrEurope) {
|
|
116
|
+
for (const list of Object.values(BUILT_IN_LOCATIONS_DATABASE)) {
|
|
117
|
+
for (const loc of list) {
|
|
118
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
119
|
+
addedSlugs.add(loc.slug);
|
|
120
|
+
results.push(loc);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const countryAliasMap: Record<string, string> = {
|
|
128
|
+
france: "fr",
|
|
129
|
+
fr: "fr",
|
|
130
|
+
spain: "es",
|
|
131
|
+
espagne: "es",
|
|
132
|
+
es: "es",
|
|
133
|
+
germany: "de",
|
|
134
|
+
allemagne: "de",
|
|
135
|
+
de: "de",
|
|
136
|
+
uk: "uk",
|
|
137
|
+
gb: "uk",
|
|
138
|
+
"united kingdom": "uk",
|
|
139
|
+
angleterre: "uk",
|
|
140
|
+
us: "us",
|
|
141
|
+
usa: "us",
|
|
142
|
+
"united states": "us",
|
|
143
|
+
etatsunis: "us",
|
|
144
|
+
belgium: "be",
|
|
145
|
+
belgique: "be",
|
|
146
|
+
be: "be",
|
|
147
|
+
switzerland: "ch",
|
|
148
|
+
suisse: "ch",
|
|
149
|
+
ch: "ch",
|
|
150
|
+
italy: "it",
|
|
151
|
+
italie: "it",
|
|
152
|
+
it: "it",
|
|
153
|
+
canada: "ca",
|
|
154
|
+
ca: "ca",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
for (const req of requested) {
|
|
158
|
+
const code = countryAliasMap[req.replace(/[^a-z0-9]/g, "")];
|
|
159
|
+
if (code && BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
160
|
+
for (const loc of BUILT_IN_LOCATIONS_DATABASE[code]) {
|
|
161
|
+
if (!addedSlugs.has(loc.slug)) {
|
|
162
|
+
addedSlugs.add(loc.slug);
|
|
163
|
+
results.push(loc);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return results.length > 0 ? results : [...BUILT_IN_LOCATIONS_DATABASE.fr];
|
|
170
|
+
}
|
package/src/engine.test.ts
CHANGED
|
@@ -186,4 +186,52 @@ describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
|
|
|
186
186
|
expect(offer["@type"]).toBe("AggregateOffer");
|
|
187
187
|
expect(offer.lowPrice).toBe("29");
|
|
188
188
|
});
|
|
189
|
+
|
|
190
|
+
it("PseoMatrixEngine: resolvePage returns complete page in < 0.05ms", () => {
|
|
191
|
+
const engine = new PseoMatrixEngine();
|
|
192
|
+
const page = engine.resolvePage(
|
|
193
|
+
["autopost-facebook", "lyon"],
|
|
194
|
+
"https://acme.com",
|
|
195
|
+
{
|
|
196
|
+
services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
|
|
197
|
+
locations: [{ slug: "lyon", name: "Lyon", country: "France", population: 500_000 }],
|
|
198
|
+
},
|
|
199
|
+
{ brandName: "Acme", cleanDirectRoutes: true },
|
|
200
|
+
);
|
|
201
|
+
expect(page).toBeDefined();
|
|
202
|
+
expect(page?.urlPath).toBe("/autopost-facebook/lyon");
|
|
203
|
+
expect(page?.h1).toContain("Autopost Facebook");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("PseoMatrixEngine: generateSitemapXml and generateLlmsTxt generate complete feeds", () => {
|
|
207
|
+
const engine = new PseoMatrixEngine();
|
|
208
|
+
const data = {
|
|
209
|
+
services: [{ slug: "autopost", name: "Autopost", category: "Social" }],
|
|
210
|
+
locations: [{ slug: "paris", name: "Paris", country: "France", population: 2_000_000 }],
|
|
211
|
+
};
|
|
212
|
+
const sitemap = engine.generateSitemapXml("https://acme.com", data, { brandName: "Acme" });
|
|
213
|
+
expect(sitemap).toContain("<urlset");
|
|
214
|
+
expect(sitemap).toContain("<loc>https://acme.com/autopost/paris</loc>");
|
|
215
|
+
|
|
216
|
+
const llmsTxt = engine.generateLlmsTxt("https://acme.com", data, { brandName: "Acme" });
|
|
217
|
+
expect(llmsTxt).toContain("# Acme — AI Knowledge Graph");
|
|
218
|
+
expect(llmsTxt).toContain("https://acme.com/autopost/paris");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("Built-in Locations: Auto-provisions cities without manual entry", () => {
|
|
222
|
+
const engine = new PseoMatrixEngine();
|
|
223
|
+
const pages = engine.generateAllMatrices(
|
|
224
|
+
"https://acme.com",
|
|
225
|
+
{
|
|
226
|
+
services: [{ slug: "autopost-facebook", name: "Autopost Facebook", category: "Social" }],
|
|
227
|
+
},
|
|
228
|
+
{ brandName: "Acme", countries: ["france", "spain"] },
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const paths = pages.map((p) => p.urlPath);
|
|
232
|
+
expect(paths).toContain("/autopost-facebook/paris");
|
|
233
|
+
expect(paths).toContain("/autopost-facebook/lyon");
|
|
234
|
+
expect(paths).toContain("/autopost-facebook/madrid");
|
|
235
|
+
expect(paths).toContain("/autopost-facebook/barcelona");
|
|
236
|
+
});
|
|
189
237
|
});
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./engine";
|
|
|
14
14
|
export * from "./slug-engine";
|
|
15
15
|
export * from "./legal-disclaimers";
|
|
16
16
|
export * from "./matrix-engine";
|
|
17
|
+
export * from "./built-in-locations";
|
|
17
18
|
export * from "./brand-icons";
|
|
18
19
|
export * from "./urlytics-engine";
|
|
19
20
|
export * from "./keyword-permutator";
|
package/src/llm-prompt.ts
CHANGED
|
@@ -37,7 +37,21 @@ When discovering product modules, features, or services to build the SEO matrice
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
###
|
|
40
|
+
### 📏 3. THE GOLDEN RULES OF URL ARCHITECTURE (MAX 3-4 SEGMENTS, ZERO PARASITE WORDS)
|
|
41
|
+
|
|
42
|
+
- **Rule A (2 to 3 URL Segments Ideal, 4 Maximum):**
|
|
43
|
+
- ✅ **2 Segments (Top SEO Performance):** \`site.com/{service}/{city}\` (e.g. \`/autopost-facebook/lyon\`, \`/crm/paris\`).
|
|
44
|
+
- ✅ **3 Segments (B2B Persona/Hub):** \`site.com/for/{target}/{city}\` or \`site.com/{locale}/{service}/{city}\`.
|
|
45
|
+
- ❌ **Never 5 to 6 Segments:** Avoid \`/solutions/ai/autopost/facebook/fr/lyon\`.
|
|
46
|
+
- **Rule B (Eliminate All Parasite Noise Words):**
|
|
47
|
+
- Banish generic wrapper words like \`/solutions/\`, \`/pages/\`, \`/items/\`. Go direct to user search intent: What you do + Platform + Location.
|
|
48
|
+
- **Rule C (The SDK Does 100% of the Heavy Lifting):**
|
|
49
|
+
- The developer/AI only registers 3-4 product features and target cities in \`lib/seo.ts\`.
|
|
50
|
+
- The SDK automatically resolves routes in memory via \`matrixEngine.resolvePage(params.slug)\`, generates complete Schema.org graphs (LocalBusiness, AggregateRating, AggregateOffer), creates AEO summaries, builds FAQ accordions, and manages mesh linking in < 0.05ms.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### 🏆 4. THE ARCHITECTURAL PLAYBOOK OF SAAS TITANS (7M+ PAGES WITH 0 DB BOTTLENECKS)
|
|
41
55
|
|
|
42
56
|
How category leaders generate millions of high-converting pages without server saturation:
|
|
43
57
|
|
|
@@ -49,18 +63,18 @@ How category leaders generate millions of high-converting pages without server s
|
|
|
49
63
|
|
|
50
64
|
2. **The 4 Pillars of Zero-Latency (< 0.05ms) Resolution:**
|
|
51
65
|
- **0 Bytes in Database:** Never generate physical static files or query heavy databases per request. Pages are resolved mathematically in RAM upon arrival.
|
|
52
|
-
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\`
|
|
53
|
-
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge
|
|
66
|
+
- **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\` handles all matrices through one lightweight layout.
|
|
67
|
+
- **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge on first request.
|
|
54
68
|
- **Automatic Siloing & Mesh Linking:**
|
|
55
69
|
- Links to 3 neighboring geographic cities for the same service.
|
|
56
70
|
- Links to 4 related professions/industries in the same target category.
|
|
57
71
|
- Complete Schema.org BreadcrumbList navigation.
|
|
58
|
-
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index
|
|
72
|
+
- **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index.
|
|
59
73
|
- **Dynamic \`/llms.txt\` Feed:** High-density Markdown stream enabling direct indexing by ChatGPT Search & Perplexity.
|
|
60
74
|
|
|
61
75
|
---
|
|
62
76
|
|
|
63
|
-
### 🛠️
|
|
77
|
+
### 🛠️ 5. AI AGENT IMPLEMENTATION PROTOCOL (STEP-BY-STEP)
|
|
64
78
|
|
|
65
79
|
When setting up programmatic SEO in any codebase, the AI Agent MUST generate these exact 4 files:
|
|
66
80
|
|
|
@@ -72,27 +86,30 @@ export const matrixEngine = new PseoMatrixEngine();
|
|
|
72
86
|
|
|
73
87
|
export const SEO_CONFIG = {
|
|
74
88
|
brandName: "Acme",
|
|
75
|
-
language: "
|
|
89
|
+
language: "fr",
|
|
90
|
+
countries: ["france", "belgique", "suisse"], // Or "europe" / "international". The SDK automatically provides verified cities!
|
|
91
|
+
cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.)
|
|
76
92
|
minPopulationToIndex: 15_000,
|
|
77
93
|
};
|
|
78
94
|
|
|
79
95
|
// Discovered EXCLUSIVELY from Frontend UI components
|
|
96
|
+
// NOTE: DO NOT list cities manually! The SDK auto-provisions them based on your target countries/scope.
|
|
80
97
|
export const PSEO_DATASET = {
|
|
81
98
|
services: [
|
|
82
99
|
// 1 per major frontend feature branch
|
|
83
|
-
{ slug: "
|
|
100
|
+
{ slug: "autopost-facebook", name: "Autopost Facebook & Instagram", category: "Social Media", keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"], description: "Automated social media posting engine" },
|
|
84
101
|
],
|
|
85
102
|
targets: [
|
|
86
|
-
{ slug: "
|
|
103
|
+
{ slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] },
|
|
87
104
|
],
|
|
88
105
|
integrations: [
|
|
89
|
-
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["
|
|
106
|
+
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] },
|
|
90
107
|
],
|
|
91
108
|
templates: [
|
|
92
|
-
{ slug: "
|
|
109
|
+
{ slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Instant setup"] },
|
|
93
110
|
],
|
|
94
111
|
glossaryTerms: [
|
|
95
|
-
{ slug: "
|
|
112
|
+
{ slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" },
|
|
96
113
|
],
|
|
97
114
|
};
|
|
98
115
|
\`\`\`
|
|
@@ -105,19 +122,15 @@ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
|
|
|
105
122
|
import { renderBrandIconSvg } from "@lynxflow/seo-engine";
|
|
106
123
|
|
|
107
124
|
export async function generateMetadata({ params }: { params: { slug: string[] } }) {
|
|
108
|
-
const path = "/" + params.slug.join("/");
|
|
109
125
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
110
|
-
const
|
|
111
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
126
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
112
127
|
if (!page) return {};
|
|
113
128
|
return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
|
|
117
|
-
const path = "/" + params.slug.join("/");
|
|
118
132
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
119
|
-
const
|
|
120
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
133
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
121
134
|
if (!page) notFound();
|
|
122
135
|
|
|
123
136
|
// Automatic Brand Icon Detection
|
package/src/matrix-engine.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { cleanSeoSlug } from "./slug-engine";
|
|
14
14
|
import { LegalDisclaimerEngine } from "./legal-disclaimers";
|
|
15
15
|
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
16
|
+
import { resolveBuiltInLocations } from "./built-in-locations";
|
|
16
17
|
|
|
17
18
|
export type MatrixFamily =
|
|
18
19
|
| "local-geo"
|
|
@@ -155,6 +156,9 @@ export interface GeneratedPageMeta {
|
|
|
155
156
|
export interface MatrixOptions {
|
|
156
157
|
brandName: string;
|
|
157
158
|
language?: string; // Default: "en", supports "fr", "de", "es", "pt", "it", etc.
|
|
159
|
+
countries?: string[] | string; // e.g. ["france", "spain"] or "europe" or "international"
|
|
160
|
+
territories?: string[] | string; // Alias for countries
|
|
161
|
+
cleanDirectRoutes?: boolean; // When true (default), eliminates parasite words (/solutions/, etc.) for direct /{service}/{city}
|
|
158
162
|
minPopulationToIndex?: number;
|
|
159
163
|
defaultCurrency?: string;
|
|
160
164
|
defaultCurrencySymbol?: string;
|
|
@@ -263,7 +267,10 @@ export class PseoMatrixEngine {
|
|
|
263
267
|
const citySlug = cleanSeoSlug(loc.slug, { language: lang });
|
|
264
268
|
const serviceSlug = cleanSeoSlug(s.slug, { language: lang });
|
|
265
269
|
|
|
266
|
-
|
|
270
|
+
// Zero parasite words: Direct /{service}/{city} or /{service}/{country}/{city} if specified
|
|
271
|
+
const urlPath = options.cleanDirectRoutes !== false
|
|
272
|
+
? `/${serviceSlug}/${citySlug}`
|
|
273
|
+
: `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
267
274
|
const fullUrl = `${cleanDomain}${urlPath}`;
|
|
268
275
|
|
|
269
276
|
const isIndexed = (loc.population ?? 20_000) >= minPop;
|
|
@@ -786,8 +793,11 @@ export class PseoMatrixEngine {
|
|
|
786
793
|
): GeneratedPageMeta[] {
|
|
787
794
|
const allPages: GeneratedPageMeta[] = [];
|
|
788
795
|
|
|
789
|
-
if (data.services
|
|
790
|
-
|
|
796
|
+
if (data.services) {
|
|
797
|
+
const locs = data.locations && data.locations.length > 0
|
|
798
|
+
? data.locations
|
|
799
|
+
: resolveBuiltInLocations(options.countries || options.territories || options.language || "fr");
|
|
800
|
+
allPages.push(...this.generateLocalGeoMatrix(domain, data.services, locs, options));
|
|
791
801
|
}
|
|
792
802
|
if (data.competitors) {
|
|
793
803
|
allPages.push(...this.generateVsMatrix(domain, data.competitors, options));
|
|
@@ -815,4 +825,83 @@ export class PseoMatrixEngine {
|
|
|
815
825
|
|
|
816
826
|
return allPages;
|
|
817
827
|
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Resolves a single page route in RAM in < 0.05ms.
|
|
831
|
+
* Matches string path or Next.js slug array.
|
|
832
|
+
*/
|
|
833
|
+
resolvePage(
|
|
834
|
+
slugOrPath: string | string[],
|
|
835
|
+
domain: string,
|
|
836
|
+
data: {
|
|
837
|
+
services?: PseoService[];
|
|
838
|
+
locations?: PseoLocation[];
|
|
839
|
+
competitors?: PseoCompetitor[];
|
|
840
|
+
targets?: PseoTarget[];
|
|
841
|
+
integrations?: PseoIntegration[];
|
|
842
|
+
useCases?: PseoUseCase[];
|
|
843
|
+
templates?: PseoTemplate[];
|
|
844
|
+
glossaryTerms?: PseoGlossaryTerm[];
|
|
845
|
+
calculators?: PseoCalculator[];
|
|
846
|
+
},
|
|
847
|
+
options: MatrixOptions,
|
|
848
|
+
): GeneratedPageMeta | undefined {
|
|
849
|
+
const path = typeof slugOrPath === "string"
|
|
850
|
+
? (slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}`)
|
|
851
|
+
: `/${slugOrPath.join("/")}`;
|
|
852
|
+
|
|
853
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
854
|
+
return pages.find((p) => p.urlPath === path);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Generates a fully formatted XML Sitemap string.
|
|
859
|
+
*/
|
|
860
|
+
generateSitemapXml(
|
|
861
|
+
domain: string,
|
|
862
|
+
data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
|
|
863
|
+
options: MatrixOptions,
|
|
864
|
+
): string {
|
|
865
|
+
const pages = this.generateAllMatrices(domain, data, options).filter((p) => p.robots.includes("index"));
|
|
866
|
+
const now = new Date().toISOString();
|
|
867
|
+
|
|
868
|
+
const urls = pages
|
|
869
|
+
.map(
|
|
870
|
+
(p) => ` <url>
|
|
871
|
+
<loc>${p.canonicalUrl}</loc>
|
|
872
|
+
<lastmod>${now}</lastmod>
|
|
873
|
+
<changefreq>weekly</changefreq>
|
|
874
|
+
<priority>0.8</priority>
|
|
875
|
+
</url>`,
|
|
876
|
+
)
|
|
877
|
+
.join("\n");
|
|
878
|
+
|
|
879
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
880
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
881
|
+
${urls}
|
|
882
|
+
</urlset>`;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Generates an official /llms.txt Markdown directory for AI search bots.
|
|
887
|
+
*/
|
|
888
|
+
generateLlmsTxt(
|
|
889
|
+
domain: string,
|
|
890
|
+
data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
|
|
891
|
+
options: MatrixOptions,
|
|
892
|
+
): string {
|
|
893
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
894
|
+
const lines: string[] = [
|
|
895
|
+
`# ${options.brandName} — AI Knowledge Graph & Page Hub`,
|
|
896
|
+
`> Complete index of services, tools, integrations, and local solution hubs.`,
|
|
897
|
+
"",
|
|
898
|
+
`## Indexed Programmatic Hubs (${pages.length} Pages)`,
|
|
899
|
+
];
|
|
900
|
+
|
|
901
|
+
for (const p of pages) {
|
|
902
|
+
lines.push(`- [${p.h1}](${p.canonicalUrl}): ${p.description}`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return lines.join("\n");
|
|
906
|
+
}
|
|
818
907
|
}
|