@lynxflow/seo-engine 1.5.5 → 1.5.7
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/extended-schemas.d.ts +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +262 -17
- package/dist/index.mjs +262 -17
- package/dist/keyword-permutator.d.ts +29 -0
- package/dist/matrix-engine.d.ts +24 -0
- package/dist/urlytics-engine.d.ts +33 -0
- package/package.json +1 -1
- package/src/engine.test.ts +84 -0
- package/src/extended-schemas.ts +98 -0
- package/src/index.ts +2 -0
- package/src/keyword-permutator.ts +118 -0
- package/src/llm-prompt.ts +31 -16
- package/src/matrix-engine.ts +84 -1
- package/src/urlytics-engine.ts +97 -0
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export * from "./slug-engine";
|
|
|
15
15
|
export * from "./legal-disclaimers";
|
|
16
16
|
export * from "./matrix-engine";
|
|
17
17
|
export * from "./brand-icons";
|
|
18
|
+
export * from "./urlytics-engine";
|
|
19
|
+
export * from "./keyword-permutator";
|
|
18
20
|
export * from "./llm-prompt";
|
|
19
21
|
export * from "./auth-key";
|
|
20
22
|
export * from "./token-quota-manager";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚡ High-Speed Keyword Permutator & SEM Matrix Engine
|
|
3
|
+
* Inspired by advertools.kw_generate
|
|
4
|
+
*
|
|
5
|
+
* Generates all permutations and combinations of Products × Modifiers × Intents × Locations
|
|
6
|
+
* with match types (Broad, Phrase, Exact) and estimated search intent tagging.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type SearchIntentType = "commercial" | "transactional" | "informational" | "navigational";
|
|
10
|
+
|
|
11
|
+
export interface GeneratedKeyword {
|
|
12
|
+
keyword: string;
|
|
13
|
+
exactMatch: string;
|
|
14
|
+
phraseMatch: string;
|
|
15
|
+
intent: SearchIntentType;
|
|
16
|
+
product: string;
|
|
17
|
+
modifier?: string;
|
|
18
|
+
location?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PermutatorOptions {
|
|
22
|
+
products: string[];
|
|
23
|
+
words?: string[];
|
|
24
|
+
locations?: string[];
|
|
25
|
+
maxCombinations?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class KeywordPermutatorEngine {
|
|
29
|
+
/**
|
|
30
|
+
* Generates a combinatorial matrix of keywords.
|
|
31
|
+
*/
|
|
32
|
+
static generateKeywordMatrix(options: PermutatorOptions): GeneratedKeyword[] {
|
|
33
|
+
const { products, words = [], locations = [], maxCombinations = 5000 } = options;
|
|
34
|
+
const results: GeneratedKeyword[] = [];
|
|
35
|
+
|
|
36
|
+
const intentKeywords: Record<SearchIntentType, string[]> = {
|
|
37
|
+
transactional: ["buy", "pricing", "cost", "hire", "quote", "tarif", "prix", "devis", "acheter"],
|
|
38
|
+
commercial: ["best", "top", "review", "vs", "comparison", "alternative", "comparatif", "meilleur"],
|
|
39
|
+
informational: ["how to", "what is", "guide", "tutorial", "definition", "comment", "quest ce que"],
|
|
40
|
+
navigational: ["login", "app", "portal", "website", "connexion"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const detectIntent = (text: string): SearchIntentType => {
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
for (const [intent, triggers] of Object.entries(intentKeywords) as [SearchIntentType, string[]][]) {
|
|
46
|
+
if (triggers.some((t) => lower.includes(t))) {
|
|
47
|
+
return intent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "commercial";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const prod of products) {
|
|
54
|
+
// 1. Product alone
|
|
55
|
+
results.push({
|
|
56
|
+
keyword: prod,
|
|
57
|
+
exactMatch: `[${prod}]`,
|
|
58
|
+
phraseMatch: `"${prod}"`,
|
|
59
|
+
intent: detectIntent(prod),
|
|
60
|
+
product: prod,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 2. Product × Words / Modifiers
|
|
64
|
+
for (const word of words) {
|
|
65
|
+
const kw1 = `${word} ${prod}`;
|
|
66
|
+
const kw2 = `${prod} ${word}`;
|
|
67
|
+
|
|
68
|
+
results.push({
|
|
69
|
+
keyword: kw1,
|
|
70
|
+
exactMatch: `[${kw1}]`,
|
|
71
|
+
phraseMatch: `"${kw1}"`,
|
|
72
|
+
intent: detectIntent(word),
|
|
73
|
+
product: prod,
|
|
74
|
+
modifier: word,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
results.push({
|
|
78
|
+
keyword: kw2,
|
|
79
|
+
exactMatch: `[${kw2}]`,
|
|
80
|
+
phraseMatch: `"${kw2}"`,
|
|
81
|
+
intent: detectIntent(word),
|
|
82
|
+
product: prod,
|
|
83
|
+
modifier: word,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 3. Product × Words × Locations
|
|
87
|
+
for (const loc of locations) {
|
|
88
|
+
const kwLoc1 = `${word} ${prod} ${loc}`;
|
|
89
|
+
const kwLoc2 = `${prod} ${loc} ${word}`;
|
|
90
|
+
|
|
91
|
+
results.push({
|
|
92
|
+
keyword: kwLoc1,
|
|
93
|
+
exactMatch: `[${kwLoc1}]`,
|
|
94
|
+
phraseMatch: `"${kwLoc1}"`,
|
|
95
|
+
intent: detectIntent(word),
|
|
96
|
+
product: prod,
|
|
97
|
+
modifier: word,
|
|
98
|
+
location: loc,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
results.push({
|
|
102
|
+
keyword: kwLoc2,
|
|
103
|
+
exactMatch: `[${kwLoc2}]`,
|
|
104
|
+
phraseMatch: `"${kwLoc2}"`,
|
|
105
|
+
intent: detectIntent(word),
|
|
106
|
+
product: prod,
|
|
107
|
+
modifier: word,
|
|
108
|
+
location: loc,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (results.length >= maxCombinations) return results;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
}
|
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
|
|
|
@@ -73,6 +87,7 @@ export const matrixEngine = new PseoMatrixEngine();
|
|
|
73
87
|
export const SEO_CONFIG = {
|
|
74
88
|
brandName: "Acme",
|
|
75
89
|
language: "en",
|
|
90
|
+
cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.)
|
|
76
91
|
minPopulationToIndex: 15_000,
|
|
77
92
|
};
|
|
78
93
|
|
|
@@ -80,19 +95,23 @@ export const SEO_CONFIG = {
|
|
|
80
95
|
export const PSEO_DATASET = {
|
|
81
96
|
services: [
|
|
82
97
|
// 1 per major frontend feature branch
|
|
83
|
-
{ slug: "
|
|
98
|
+
{ slug: "autopost-facebook", name: "Autopost Facebook & Instagram", category: "Social Media", keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"], description: "Automated social media posting engine" },
|
|
99
|
+
],
|
|
100
|
+
locations: [
|
|
101
|
+
{ slug: "paris", name: "Paris", region: "Île-de-France", country: "France", population: 2_161_000, currency: "EUR", currencySymbol: "€" },
|
|
102
|
+
{ slug: "lyon", name: "Lyon", region: "Rhône-Alpes", country: "France", population: 522_000, currency: "EUR", currencySymbol: "€" },
|
|
84
103
|
],
|
|
85
104
|
targets: [
|
|
86
|
-
{ slug: "
|
|
105
|
+
{ slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] },
|
|
87
106
|
],
|
|
88
107
|
integrations: [
|
|
89
|
-
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["
|
|
108
|
+
{ slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] },
|
|
90
109
|
],
|
|
91
110
|
templates: [
|
|
92
|
-
{ slug: "
|
|
111
|
+
{ slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Instant setup"] },
|
|
93
112
|
],
|
|
94
113
|
glossaryTerms: [
|
|
95
|
-
{ slug: "
|
|
114
|
+
{ slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" },
|
|
96
115
|
],
|
|
97
116
|
};
|
|
98
117
|
\`\`\`
|
|
@@ -105,19 +124,15 @@ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
|
|
|
105
124
|
import { renderBrandIconSvg } from "@lynxflow/seo-engine";
|
|
106
125
|
|
|
107
126
|
export async function generateMetadata({ params }: { params: { slug: string[] } }) {
|
|
108
|
-
const path = "/" + params.slug.join("/");
|
|
109
127
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
110
|
-
const
|
|
111
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
128
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
112
129
|
if (!page) return {};
|
|
113
130
|
return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
|
|
117
|
-
const path = "/" + params.slug.join("/");
|
|
118
134
|
const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
|
|
119
|
-
const
|
|
120
|
-
const page = pages.find((p) => p.urlPath === path);
|
|
135
|
+
const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
|
|
121
136
|
if (!page) notFound();
|
|
122
137
|
|
|
123
138
|
// Automatic Brand Icon Detection
|
package/src/matrix-engine.ts
CHANGED
|
@@ -155,6 +155,7 @@ export interface GeneratedPageMeta {
|
|
|
155
155
|
export interface MatrixOptions {
|
|
156
156
|
brandName: string;
|
|
157
157
|
language?: string; // Default: "en", supports "fr", "de", "es", "pt", "it", etc.
|
|
158
|
+
cleanDirectRoutes?: boolean; // When true (default), eliminates parasite words (/solutions/, etc.) for direct /{service}/{city}
|
|
158
159
|
minPopulationToIndex?: number;
|
|
159
160
|
defaultCurrency?: string;
|
|
160
161
|
defaultCurrencySymbol?: string;
|
|
@@ -263,7 +264,10 @@ export class PseoMatrixEngine {
|
|
|
263
264
|
const citySlug = cleanSeoSlug(loc.slug, { language: lang });
|
|
264
265
|
const serviceSlug = cleanSeoSlug(s.slug, { language: lang });
|
|
265
266
|
|
|
266
|
-
|
|
267
|
+
// Zero parasite words: Direct /{service}/{city} or /{service}/{country}/{city} if specified
|
|
268
|
+
const urlPath = options.cleanDirectRoutes !== false
|
|
269
|
+
? `/${serviceSlug}/${citySlug}`
|
|
270
|
+
: `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
|
|
267
271
|
const fullUrl = `${cleanDomain}${urlPath}`;
|
|
268
272
|
|
|
269
273
|
const isIndexed = (loc.population ?? 20_000) >= minPop;
|
|
@@ -815,4 +819,83 @@ export class PseoMatrixEngine {
|
|
|
815
819
|
|
|
816
820
|
return allPages;
|
|
817
821
|
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Resolves a single page route in RAM in < 0.05ms.
|
|
825
|
+
* Matches string path or Next.js slug array.
|
|
826
|
+
*/
|
|
827
|
+
resolvePage(
|
|
828
|
+
slugOrPath: string | string[],
|
|
829
|
+
domain: string,
|
|
830
|
+
data: {
|
|
831
|
+
services?: PseoService[];
|
|
832
|
+
locations?: PseoLocation[];
|
|
833
|
+
competitors?: PseoCompetitor[];
|
|
834
|
+
targets?: PseoTarget[];
|
|
835
|
+
integrations?: PseoIntegration[];
|
|
836
|
+
useCases?: PseoUseCase[];
|
|
837
|
+
templates?: PseoTemplate[];
|
|
838
|
+
glossaryTerms?: PseoGlossaryTerm[];
|
|
839
|
+
calculators?: PseoCalculator[];
|
|
840
|
+
},
|
|
841
|
+
options: MatrixOptions,
|
|
842
|
+
): GeneratedPageMeta | undefined {
|
|
843
|
+
const path = typeof slugOrPath === "string"
|
|
844
|
+
? (slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}`)
|
|
845
|
+
: `/${slugOrPath.join("/")}`;
|
|
846
|
+
|
|
847
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
848
|
+
return pages.find((p) => p.urlPath === path);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Generates a fully formatted XML Sitemap string.
|
|
853
|
+
*/
|
|
854
|
+
generateSitemapXml(
|
|
855
|
+
domain: string,
|
|
856
|
+
data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
|
|
857
|
+
options: MatrixOptions,
|
|
858
|
+
): string {
|
|
859
|
+
const pages = this.generateAllMatrices(domain, data, options).filter((p) => p.robots.includes("index"));
|
|
860
|
+
const now = new Date().toISOString();
|
|
861
|
+
|
|
862
|
+
const urls = pages
|
|
863
|
+
.map(
|
|
864
|
+
(p) => ` <url>
|
|
865
|
+
<loc>${p.canonicalUrl}</loc>
|
|
866
|
+
<lastmod>${now}</lastmod>
|
|
867
|
+
<changefreq>weekly</changefreq>
|
|
868
|
+
<priority>0.8</priority>
|
|
869
|
+
</url>`,
|
|
870
|
+
)
|
|
871
|
+
.join("\n");
|
|
872
|
+
|
|
873
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
874
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
875
|
+
${urls}
|
|
876
|
+
</urlset>`;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Generates an official /llms.txt Markdown directory for AI search bots.
|
|
881
|
+
*/
|
|
882
|
+
generateLlmsTxt(
|
|
883
|
+
domain: string,
|
|
884
|
+
data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
|
|
885
|
+
options: MatrixOptions,
|
|
886
|
+
): string {
|
|
887
|
+
const pages = this.generateAllMatrices(domain, data, options);
|
|
888
|
+
const lines: string[] = [
|
|
889
|
+
`# ${options.brandName} — AI Knowledge Graph & Page Hub`,
|
|
890
|
+
`> Complete index of services, tools, integrations, and local solution hubs.`,
|
|
891
|
+
"",
|
|
892
|
+
`## Indexed Programmatic Hubs (${pages.length} Pages)`,
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
for (const p of pages) {
|
|
896
|
+
lines.push(`- [${p.h1}](${p.canonicalUrl}): ${p.description}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return lines.join("\n");
|
|
900
|
+
}
|
|
818
901
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔍 Universal URL Analytics & Structural Decomposition Engine
|
|
3
|
+
* Inspired by advertools.urlytics
|
|
4
|
+
*
|
|
5
|
+
* Breaks down any URL into structured path segments, query parameters, directory depths,
|
|
6
|
+
* and slug tokens for crawl validation, audit logs, and programmatic matrix matching.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ParsedUrlStructure {
|
|
10
|
+
url: string;
|
|
11
|
+
scheme: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
path: string;
|
|
14
|
+
depth: number;
|
|
15
|
+
dir1?: string;
|
|
16
|
+
dir2?: string;
|
|
17
|
+
dir3?: string;
|
|
18
|
+
lastDir: string;
|
|
19
|
+
queryParams: Record<string, string>;
|
|
20
|
+
hashFragment?: string;
|
|
21
|
+
slugTokens: string[];
|
|
22
|
+
charCount: number;
|
|
23
|
+
hasTrailingSlash: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class UrlyticsEngine {
|
|
27
|
+
/**
|
|
28
|
+
* Parses a single URL into its granular structural components.
|
|
29
|
+
*/
|
|
30
|
+
static parseUrl(rawUrl: string): ParsedUrlStructure {
|
|
31
|
+
if (!rawUrl || typeof rawUrl !== "string") {
|
|
32
|
+
return {
|
|
33
|
+
url: "",
|
|
34
|
+
scheme: "",
|
|
35
|
+
domain: "",
|
|
36
|
+
path: "/",
|
|
37
|
+
depth: 0,
|
|
38
|
+
lastDir: "",
|
|
39
|
+
queryParams: {},
|
|
40
|
+
slugTokens: [],
|
|
41
|
+
charCount: 0,
|
|
42
|
+
hasTrailingSlash: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`);
|
|
48
|
+
const pathClean = parsed.pathname.replace(/\/+$/, "");
|
|
49
|
+
const segments = pathClean.split("/").filter(Boolean);
|
|
50
|
+
const queryParams: Record<string, string> = {};
|
|
51
|
+
|
|
52
|
+
parsed.searchParams.forEach((val, key) => {
|
|
53
|
+
queryParams[key] = val;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const lastDir = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
57
|
+
const slugTokens = lastDir.split("-").filter(Boolean);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
url: rawUrl,
|
|
61
|
+
scheme: parsed.protocol.replace(":", ""),
|
|
62
|
+
domain: parsed.hostname,
|
|
63
|
+
path: parsed.pathname,
|
|
64
|
+
depth: segments.length,
|
|
65
|
+
dir1: segments[0],
|
|
66
|
+
dir2: segments[1],
|
|
67
|
+
dir3: segments[2],
|
|
68
|
+
lastDir,
|
|
69
|
+
queryParams,
|
|
70
|
+
hashFragment: parsed.hash ? parsed.hash.replace("#", "") : undefined,
|
|
71
|
+
slugTokens,
|
|
72
|
+
charCount: rawUrl.length,
|
|
73
|
+
hasTrailingSlash: parsed.pathname.length > 1 && parsed.pathname.endsWith("/"),
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return {
|
|
77
|
+
url: rawUrl,
|
|
78
|
+
scheme: "unknown",
|
|
79
|
+
domain: "",
|
|
80
|
+
path: rawUrl,
|
|
81
|
+
depth: 0,
|
|
82
|
+
lastDir: rawUrl,
|
|
83
|
+
queryParams: {},
|
|
84
|
+
slugTokens: [rawUrl],
|
|
85
|
+
charCount: rawUrl.length,
|
|
86
|
+
hasTrailingSlash: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Batch processes an array of URLs for comparative directory analysis.
|
|
93
|
+
*/
|
|
94
|
+
static analyzeUrls(urls: string[]): ParsedUrlStructure[] {
|
|
95
|
+
return urls.map((u) => this.parseUrl(u));
|
|
96
|
+
}
|
|
97
|
+
}
|