@lynxflow/seo-engine 1.5.5 → 1.5.6
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 +196 -0
- package/dist/index.mjs +196 -0
- package/dist/keyword-permutator.d.ts +29 -0
- package/dist/urlytics-engine.d.ts +33 -0
- package/package.json +1 -1
- package/src/engine.test.ts +53 -0
- package/src/extended-schemas.ts +98 -0
- package/src/index.ts +2 -0
- package/src/keyword-permutator.ts +118 -0
- package/src/urlytics-engine.ts +97 -0
|
@@ -103,4 +103,42 @@ export declare class ExtendedSchemaGraphBuilder {
|
|
|
103
103
|
name: string;
|
|
104
104
|
url: string;
|
|
105
105
|
}[]): Record<string, unknown>;
|
|
106
|
+
/**
|
|
107
|
+
* 7. AggregateRating (Google Gold Stars in SERPs) - Inspired by santifer-irepair
|
|
108
|
+
*/
|
|
109
|
+
static buildAggregateRating(opts: {
|
|
110
|
+
ratingValue?: number;
|
|
111
|
+
reviewCount?: number;
|
|
112
|
+
bestRating?: number;
|
|
113
|
+
worstRating?: number;
|
|
114
|
+
itemReviewedName: string;
|
|
115
|
+
}): Record<string, unknown>;
|
|
116
|
+
/**
|
|
117
|
+
* 8. AggregateOffer (Price Ranges & Tiers) - Inspired by santifer-irepair
|
|
118
|
+
*/
|
|
119
|
+
static buildAggregateOffer(opts: {
|
|
120
|
+
lowPrice: number;
|
|
121
|
+
highPrice: number;
|
|
122
|
+
currency: string;
|
|
123
|
+
offerCount?: number;
|
|
124
|
+
description?: string;
|
|
125
|
+
}): Record<string, unknown>;
|
|
126
|
+
/**
|
|
127
|
+
* 9. Service with ServiceArea & Providers
|
|
128
|
+
*/
|
|
129
|
+
static buildService(opts: {
|
|
130
|
+
name: string;
|
|
131
|
+
description: string;
|
|
132
|
+
providerName: string;
|
|
133
|
+
areaServed?: string[];
|
|
134
|
+
serviceType?: string;
|
|
135
|
+
}): Record<string, unknown>;
|
|
136
|
+
/**
|
|
137
|
+
* 10. WebSite with Google Sitelinks SearchBox
|
|
138
|
+
*/
|
|
139
|
+
static buildWebSite(opts: {
|
|
140
|
+
name: string;
|
|
141
|
+
url: string;
|
|
142
|
+
searchUrlTemplate?: string;
|
|
143
|
+
}): Record<string, unknown>;
|
|
106
144
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from "./slug-engine";
|
|
|
14
14
|
export * from "./legal-disclaimers";
|
|
15
15
|
export * from "./matrix-engine";
|
|
16
16
|
export * from "./brand-icons";
|
|
17
|
+
export * from "./urlytics-engine";
|
|
18
|
+
export * from "./keyword-permutator";
|
|
17
19
|
export * from "./llm-prompt";
|
|
18
20
|
export * from "./auth-key";
|
|
19
21
|
export * from "./token-quota-manager";
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ __export(exports_src, {
|
|
|
47
47
|
createLynxSeoEngine: () => createLynxSeoEngine,
|
|
48
48
|
cleanSeoSlug: () => cleanSeoSlug,
|
|
49
49
|
YoastParityEngine: () => YoastParityEngine,
|
|
50
|
+
UrlyticsEngine: () => UrlyticsEngine,
|
|
50
51
|
TokenQuotaManager: () => TokenQuotaManager,
|
|
51
52
|
TechnicalRulesAuditor: () => TechnicalRulesAuditor,
|
|
52
53
|
TeamRbacEngine: () => TeamRbacEngine,
|
|
@@ -78,6 +79,7 @@ __export(exports_src, {
|
|
|
78
79
|
LegalDisclaimerEngine: () => LegalDisclaimerEngine,
|
|
79
80
|
LagoTokenMeter: () => LagoTokenMeter,
|
|
80
81
|
KnowledgeGraphLinker: () => KnowledgeGraphLinker,
|
|
82
|
+
KeywordPermutatorEngine: () => KeywordPermutatorEngine,
|
|
81
83
|
IsrCacheManager: () => IsrCacheManager,
|
|
82
84
|
InstantMatrixSearchEngine: () => InstantMatrixSearchEngine,
|
|
83
85
|
IndexNowClient: () => IndexNowClient,
|
|
@@ -3130,6 +3132,62 @@ class ExtendedSchemaGraphBuilder {
|
|
|
3130
3132
|
}))
|
|
3131
3133
|
};
|
|
3132
3134
|
}
|
|
3135
|
+
static buildAggregateRating(opts) {
|
|
3136
|
+
return {
|
|
3137
|
+
"@type": "AggregateRating",
|
|
3138
|
+
ratingValue: opts.ratingValue ?? 4.9,
|
|
3139
|
+
reviewCount: opts.reviewCount ?? 1280,
|
|
3140
|
+
bestRating: opts.bestRating ?? 5,
|
|
3141
|
+
worstRating: opts.worstRating ?? 1,
|
|
3142
|
+
itemReviewed: {
|
|
3143
|
+
"@type": "Thing",
|
|
3144
|
+
name: opts.itemReviewedName
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
static buildAggregateOffer(opts) {
|
|
3149
|
+
return {
|
|
3150
|
+
"@type": "AggregateOffer",
|
|
3151
|
+
lowPrice: String(opts.lowPrice),
|
|
3152
|
+
highPrice: String(opts.highPrice),
|
|
3153
|
+
priceCurrency: opts.currency,
|
|
3154
|
+
offerCount: String(opts.offerCount ?? 3),
|
|
3155
|
+
description: opts.description || "Transparent pricing with zero lock-in"
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
static buildService(opts) {
|
|
3159
|
+
return {
|
|
3160
|
+
"@context": "https://schema.org",
|
|
3161
|
+
"@type": "Service",
|
|
3162
|
+
name: opts.name,
|
|
3163
|
+
description: opts.description,
|
|
3164
|
+
serviceType: opts.serviceType || "SoftwareService",
|
|
3165
|
+
provider: {
|
|
3166
|
+
"@type": "Organization",
|
|
3167
|
+
name: opts.providerName
|
|
3168
|
+
},
|
|
3169
|
+
areaServed: opts.areaServed ? opts.areaServed.map((city) => ({
|
|
3170
|
+
"@type": "City",
|
|
3171
|
+
name: city
|
|
3172
|
+
})) : undefined
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
static buildWebSite(opts) {
|
|
3176
|
+
return {
|
|
3177
|
+
"@context": "https://schema.org",
|
|
3178
|
+
"@type": "WebSite",
|
|
3179
|
+
name: opts.name,
|
|
3180
|
+
url: opts.url,
|
|
3181
|
+
potentialAction: opts.searchUrlTemplate ? {
|
|
3182
|
+
"@type": "SearchAction",
|
|
3183
|
+
target: {
|
|
3184
|
+
"@type": "EntryPoint",
|
|
3185
|
+
urlTemplate: opts.searchUrlTemplate
|
|
3186
|
+
},
|
|
3187
|
+
"query-input": "required name=search_term_string"
|
|
3188
|
+
} : undefined
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3133
3191
|
}
|
|
3134
3192
|
|
|
3135
3193
|
// src/matrix-engine.ts
|
|
@@ -3754,6 +3812,144 @@ function renderBrandIconSvg(input, className = "w-5 h-5 inline-block") {
|
|
|
3754
3812
|
return "";
|
|
3755
3813
|
return `<svg class="${className}" viewBox="${icon.viewBox}" fill="currentColor" aria-hidden="true"><path d="${icon.svgPath}"/></svg>`;
|
|
3756
3814
|
}
|
|
3815
|
+
// src/urlytics-engine.ts
|
|
3816
|
+
class UrlyticsEngine {
|
|
3817
|
+
static parseUrl(rawUrl) {
|
|
3818
|
+
if (!rawUrl || typeof rawUrl !== "string") {
|
|
3819
|
+
return {
|
|
3820
|
+
url: "",
|
|
3821
|
+
scheme: "",
|
|
3822
|
+
domain: "",
|
|
3823
|
+
path: "/",
|
|
3824
|
+
depth: 0,
|
|
3825
|
+
lastDir: "",
|
|
3826
|
+
queryParams: {},
|
|
3827
|
+
slugTokens: [],
|
|
3828
|
+
charCount: 0,
|
|
3829
|
+
hasTrailingSlash: false
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
try {
|
|
3833
|
+
const parsed = new URL(rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`);
|
|
3834
|
+
const pathClean = parsed.pathname.replace(/\/+$/, "");
|
|
3835
|
+
const segments = pathClean.split("/").filter(Boolean);
|
|
3836
|
+
const queryParams = {};
|
|
3837
|
+
parsed.searchParams.forEach((val, key) => {
|
|
3838
|
+
queryParams[key] = val;
|
|
3839
|
+
});
|
|
3840
|
+
const lastDir = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
3841
|
+
const slugTokens = lastDir.split("-").filter(Boolean);
|
|
3842
|
+
return {
|
|
3843
|
+
url: rawUrl,
|
|
3844
|
+
scheme: parsed.protocol.replace(":", ""),
|
|
3845
|
+
domain: parsed.hostname,
|
|
3846
|
+
path: parsed.pathname,
|
|
3847
|
+
depth: segments.length,
|
|
3848
|
+
dir1: segments[0],
|
|
3849
|
+
dir2: segments[1],
|
|
3850
|
+
dir3: segments[2],
|
|
3851
|
+
lastDir,
|
|
3852
|
+
queryParams,
|
|
3853
|
+
hashFragment: parsed.hash ? parsed.hash.replace("#", "") : undefined,
|
|
3854
|
+
slugTokens,
|
|
3855
|
+
charCount: rawUrl.length,
|
|
3856
|
+
hasTrailingSlash: parsed.pathname.length > 1 && parsed.pathname.endsWith("/")
|
|
3857
|
+
};
|
|
3858
|
+
} catch {
|
|
3859
|
+
return {
|
|
3860
|
+
url: rawUrl,
|
|
3861
|
+
scheme: "unknown",
|
|
3862
|
+
domain: "",
|
|
3863
|
+
path: rawUrl,
|
|
3864
|
+
depth: 0,
|
|
3865
|
+
lastDir: rawUrl,
|
|
3866
|
+
queryParams: {},
|
|
3867
|
+
slugTokens: [rawUrl],
|
|
3868
|
+
charCount: rawUrl.length,
|
|
3869
|
+
hasTrailingSlash: false
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
static analyzeUrls(urls) {
|
|
3874
|
+
return urls.map((u) => this.parseUrl(u));
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
// src/keyword-permutator.ts
|
|
3878
|
+
class KeywordPermutatorEngine {
|
|
3879
|
+
static generateKeywordMatrix(options) {
|
|
3880
|
+
const { products, words = [], locations = [], maxCombinations = 5000 } = options;
|
|
3881
|
+
const results = [];
|
|
3882
|
+
const intentKeywords = {
|
|
3883
|
+
transactional: ["buy", "pricing", "cost", "hire", "quote", "tarif", "prix", "devis", "acheter"],
|
|
3884
|
+
commercial: ["best", "top", "review", "vs", "comparison", "alternative", "comparatif", "meilleur"],
|
|
3885
|
+
informational: ["how to", "what is", "guide", "tutorial", "definition", "comment", "quest ce que"],
|
|
3886
|
+
navigational: ["login", "app", "portal", "website", "connexion"]
|
|
3887
|
+
};
|
|
3888
|
+
const detectIntent = (text) => {
|
|
3889
|
+
const lower = text.toLowerCase();
|
|
3890
|
+
for (const [intent, triggers] of Object.entries(intentKeywords)) {
|
|
3891
|
+
if (triggers.some((t) => lower.includes(t))) {
|
|
3892
|
+
return intent;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
return "commercial";
|
|
3896
|
+
};
|
|
3897
|
+
for (const prod of products) {
|
|
3898
|
+
results.push({
|
|
3899
|
+
keyword: prod,
|
|
3900
|
+
exactMatch: `[${prod}]`,
|
|
3901
|
+
phraseMatch: `"${prod}"`,
|
|
3902
|
+
intent: detectIntent(prod),
|
|
3903
|
+
product: prod
|
|
3904
|
+
});
|
|
3905
|
+
for (const word of words) {
|
|
3906
|
+
const kw1 = `${word} ${prod}`;
|
|
3907
|
+
const kw2 = `${prod} ${word}`;
|
|
3908
|
+
results.push({
|
|
3909
|
+
keyword: kw1,
|
|
3910
|
+
exactMatch: `[${kw1}]`,
|
|
3911
|
+
phraseMatch: `"${kw1}"`,
|
|
3912
|
+
intent: detectIntent(word),
|
|
3913
|
+
product: prod,
|
|
3914
|
+
modifier: word
|
|
3915
|
+
});
|
|
3916
|
+
results.push({
|
|
3917
|
+
keyword: kw2,
|
|
3918
|
+
exactMatch: `[${kw2}]`,
|
|
3919
|
+
phraseMatch: `"${kw2}"`,
|
|
3920
|
+
intent: detectIntent(word),
|
|
3921
|
+
product: prod,
|
|
3922
|
+
modifier: word
|
|
3923
|
+
});
|
|
3924
|
+
for (const loc of locations) {
|
|
3925
|
+
const kwLoc1 = `${word} ${prod} ${loc}`;
|
|
3926
|
+
const kwLoc2 = `${prod} ${loc} ${word}`;
|
|
3927
|
+
results.push({
|
|
3928
|
+
keyword: kwLoc1,
|
|
3929
|
+
exactMatch: `[${kwLoc1}]`,
|
|
3930
|
+
phraseMatch: `"${kwLoc1}"`,
|
|
3931
|
+
intent: detectIntent(word),
|
|
3932
|
+
product: prod,
|
|
3933
|
+
modifier: word,
|
|
3934
|
+
location: loc
|
|
3935
|
+
});
|
|
3936
|
+
results.push({
|
|
3937
|
+
keyword: kwLoc2,
|
|
3938
|
+
exactMatch: `[${kwLoc2}]`,
|
|
3939
|
+
phraseMatch: `"${kwLoc2}"`,
|
|
3940
|
+
intent: detectIntent(word),
|
|
3941
|
+
product: prod,
|
|
3942
|
+
modifier: word,
|
|
3943
|
+
location: loc
|
|
3944
|
+
});
|
|
3945
|
+
if (results.length >= maxCombinations)
|
|
3946
|
+
return results;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
return results;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3757
3953
|
// src/llm-prompt.ts
|
|
3758
3954
|
var PSEO_AGENT_SYSTEM_PROMPT = `
|
|
3759
3955
|
You are the Programmatic SEO Implementation Specialist powered by @lynxflow/seo-engine.
|
package/dist/index.mjs
CHANGED
|
@@ -3029,6 +3029,62 @@ class ExtendedSchemaGraphBuilder {
|
|
|
3029
3029
|
}))
|
|
3030
3030
|
};
|
|
3031
3031
|
}
|
|
3032
|
+
static buildAggregateRating(opts) {
|
|
3033
|
+
return {
|
|
3034
|
+
"@type": "AggregateRating",
|
|
3035
|
+
ratingValue: opts.ratingValue ?? 4.9,
|
|
3036
|
+
reviewCount: opts.reviewCount ?? 1280,
|
|
3037
|
+
bestRating: opts.bestRating ?? 5,
|
|
3038
|
+
worstRating: opts.worstRating ?? 1,
|
|
3039
|
+
itemReviewed: {
|
|
3040
|
+
"@type": "Thing",
|
|
3041
|
+
name: opts.itemReviewedName
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
static buildAggregateOffer(opts) {
|
|
3046
|
+
return {
|
|
3047
|
+
"@type": "AggregateOffer",
|
|
3048
|
+
lowPrice: String(opts.lowPrice),
|
|
3049
|
+
highPrice: String(opts.highPrice),
|
|
3050
|
+
priceCurrency: opts.currency,
|
|
3051
|
+
offerCount: String(opts.offerCount ?? 3),
|
|
3052
|
+
description: opts.description || "Transparent pricing with zero lock-in"
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
static buildService(opts) {
|
|
3056
|
+
return {
|
|
3057
|
+
"@context": "https://schema.org",
|
|
3058
|
+
"@type": "Service",
|
|
3059
|
+
name: opts.name,
|
|
3060
|
+
description: opts.description,
|
|
3061
|
+
serviceType: opts.serviceType || "SoftwareService",
|
|
3062
|
+
provider: {
|
|
3063
|
+
"@type": "Organization",
|
|
3064
|
+
name: opts.providerName
|
|
3065
|
+
},
|
|
3066
|
+
areaServed: opts.areaServed ? opts.areaServed.map((city) => ({
|
|
3067
|
+
"@type": "City",
|
|
3068
|
+
name: city
|
|
3069
|
+
})) : undefined
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
static buildWebSite(opts) {
|
|
3073
|
+
return {
|
|
3074
|
+
"@context": "https://schema.org",
|
|
3075
|
+
"@type": "WebSite",
|
|
3076
|
+
name: opts.name,
|
|
3077
|
+
url: opts.url,
|
|
3078
|
+
potentialAction: opts.searchUrlTemplate ? {
|
|
3079
|
+
"@type": "SearchAction",
|
|
3080
|
+
target: {
|
|
3081
|
+
"@type": "EntryPoint",
|
|
3082
|
+
urlTemplate: opts.searchUrlTemplate
|
|
3083
|
+
},
|
|
3084
|
+
"query-input": "required name=search_term_string"
|
|
3085
|
+
} : undefined
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3032
3088
|
}
|
|
3033
3089
|
|
|
3034
3090
|
// src/matrix-engine.ts
|
|
@@ -3653,6 +3709,144 @@ function renderBrandIconSvg(input, className = "w-5 h-5 inline-block") {
|
|
|
3653
3709
|
return "";
|
|
3654
3710
|
return `<svg class="${className}" viewBox="${icon.viewBox}" fill="currentColor" aria-hidden="true"><path d="${icon.svgPath}"/></svg>`;
|
|
3655
3711
|
}
|
|
3712
|
+
// src/urlytics-engine.ts
|
|
3713
|
+
class UrlyticsEngine {
|
|
3714
|
+
static parseUrl(rawUrl) {
|
|
3715
|
+
if (!rawUrl || typeof rawUrl !== "string") {
|
|
3716
|
+
return {
|
|
3717
|
+
url: "",
|
|
3718
|
+
scheme: "",
|
|
3719
|
+
domain: "",
|
|
3720
|
+
path: "/",
|
|
3721
|
+
depth: 0,
|
|
3722
|
+
lastDir: "",
|
|
3723
|
+
queryParams: {},
|
|
3724
|
+
slugTokens: [],
|
|
3725
|
+
charCount: 0,
|
|
3726
|
+
hasTrailingSlash: false
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
try {
|
|
3730
|
+
const parsed = new URL(rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`);
|
|
3731
|
+
const pathClean = parsed.pathname.replace(/\/+$/, "");
|
|
3732
|
+
const segments = pathClean.split("/").filter(Boolean);
|
|
3733
|
+
const queryParams = {};
|
|
3734
|
+
parsed.searchParams.forEach((val, key) => {
|
|
3735
|
+
queryParams[key] = val;
|
|
3736
|
+
});
|
|
3737
|
+
const lastDir = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
3738
|
+
const slugTokens = lastDir.split("-").filter(Boolean);
|
|
3739
|
+
return {
|
|
3740
|
+
url: rawUrl,
|
|
3741
|
+
scheme: parsed.protocol.replace(":", ""),
|
|
3742
|
+
domain: parsed.hostname,
|
|
3743
|
+
path: parsed.pathname,
|
|
3744
|
+
depth: segments.length,
|
|
3745
|
+
dir1: segments[0],
|
|
3746
|
+
dir2: segments[1],
|
|
3747
|
+
dir3: segments[2],
|
|
3748
|
+
lastDir,
|
|
3749
|
+
queryParams,
|
|
3750
|
+
hashFragment: parsed.hash ? parsed.hash.replace("#", "") : undefined,
|
|
3751
|
+
slugTokens,
|
|
3752
|
+
charCount: rawUrl.length,
|
|
3753
|
+
hasTrailingSlash: parsed.pathname.length > 1 && parsed.pathname.endsWith("/")
|
|
3754
|
+
};
|
|
3755
|
+
} catch {
|
|
3756
|
+
return {
|
|
3757
|
+
url: rawUrl,
|
|
3758
|
+
scheme: "unknown",
|
|
3759
|
+
domain: "",
|
|
3760
|
+
path: rawUrl,
|
|
3761
|
+
depth: 0,
|
|
3762
|
+
lastDir: rawUrl,
|
|
3763
|
+
queryParams: {},
|
|
3764
|
+
slugTokens: [rawUrl],
|
|
3765
|
+
charCount: rawUrl.length,
|
|
3766
|
+
hasTrailingSlash: false
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
static analyzeUrls(urls) {
|
|
3771
|
+
return urls.map((u) => this.parseUrl(u));
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
// src/keyword-permutator.ts
|
|
3775
|
+
class KeywordPermutatorEngine {
|
|
3776
|
+
static generateKeywordMatrix(options) {
|
|
3777
|
+
const { products, words = [], locations = [], maxCombinations = 5000 } = options;
|
|
3778
|
+
const results = [];
|
|
3779
|
+
const intentKeywords = {
|
|
3780
|
+
transactional: ["buy", "pricing", "cost", "hire", "quote", "tarif", "prix", "devis", "acheter"],
|
|
3781
|
+
commercial: ["best", "top", "review", "vs", "comparison", "alternative", "comparatif", "meilleur"],
|
|
3782
|
+
informational: ["how to", "what is", "guide", "tutorial", "definition", "comment", "quest ce que"],
|
|
3783
|
+
navigational: ["login", "app", "portal", "website", "connexion"]
|
|
3784
|
+
};
|
|
3785
|
+
const detectIntent = (text) => {
|
|
3786
|
+
const lower = text.toLowerCase();
|
|
3787
|
+
for (const [intent, triggers] of Object.entries(intentKeywords)) {
|
|
3788
|
+
if (triggers.some((t) => lower.includes(t))) {
|
|
3789
|
+
return intent;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return "commercial";
|
|
3793
|
+
};
|
|
3794
|
+
for (const prod of products) {
|
|
3795
|
+
results.push({
|
|
3796
|
+
keyword: prod,
|
|
3797
|
+
exactMatch: `[${prod}]`,
|
|
3798
|
+
phraseMatch: `"${prod}"`,
|
|
3799
|
+
intent: detectIntent(prod),
|
|
3800
|
+
product: prod
|
|
3801
|
+
});
|
|
3802
|
+
for (const word of words) {
|
|
3803
|
+
const kw1 = `${word} ${prod}`;
|
|
3804
|
+
const kw2 = `${prod} ${word}`;
|
|
3805
|
+
results.push({
|
|
3806
|
+
keyword: kw1,
|
|
3807
|
+
exactMatch: `[${kw1}]`,
|
|
3808
|
+
phraseMatch: `"${kw1}"`,
|
|
3809
|
+
intent: detectIntent(word),
|
|
3810
|
+
product: prod,
|
|
3811
|
+
modifier: word
|
|
3812
|
+
});
|
|
3813
|
+
results.push({
|
|
3814
|
+
keyword: kw2,
|
|
3815
|
+
exactMatch: `[${kw2}]`,
|
|
3816
|
+
phraseMatch: `"${kw2}"`,
|
|
3817
|
+
intent: detectIntent(word),
|
|
3818
|
+
product: prod,
|
|
3819
|
+
modifier: word
|
|
3820
|
+
});
|
|
3821
|
+
for (const loc of locations) {
|
|
3822
|
+
const kwLoc1 = `${word} ${prod} ${loc}`;
|
|
3823
|
+
const kwLoc2 = `${prod} ${loc} ${word}`;
|
|
3824
|
+
results.push({
|
|
3825
|
+
keyword: kwLoc1,
|
|
3826
|
+
exactMatch: `[${kwLoc1}]`,
|
|
3827
|
+
phraseMatch: `"${kwLoc1}"`,
|
|
3828
|
+
intent: detectIntent(word),
|
|
3829
|
+
product: prod,
|
|
3830
|
+
modifier: word,
|
|
3831
|
+
location: loc
|
|
3832
|
+
});
|
|
3833
|
+
results.push({
|
|
3834
|
+
keyword: kwLoc2,
|
|
3835
|
+
exactMatch: `[${kwLoc2}]`,
|
|
3836
|
+
phraseMatch: `"${kwLoc2}"`,
|
|
3837
|
+
intent: detectIntent(word),
|
|
3838
|
+
product: prod,
|
|
3839
|
+
modifier: word,
|
|
3840
|
+
location: loc
|
|
3841
|
+
});
|
|
3842
|
+
if (results.length >= maxCombinations)
|
|
3843
|
+
return results;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return results;
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3656
3850
|
// src/llm-prompt.ts
|
|
3657
3851
|
var PSEO_AGENT_SYSTEM_PROMPT = `
|
|
3658
3852
|
You are the Programmatic SEO Implementation Specialist powered by @lynxflow/seo-engine.
|
|
@@ -6064,6 +6258,7 @@ export {
|
|
|
6064
6258
|
createLynxSeoEngine,
|
|
6065
6259
|
cleanSeoSlug,
|
|
6066
6260
|
YoastParityEngine,
|
|
6261
|
+
UrlyticsEngine,
|
|
6067
6262
|
TokenQuotaManager,
|
|
6068
6263
|
TechnicalRulesAuditor,
|
|
6069
6264
|
TeamRbacEngine,
|
|
@@ -6095,6 +6290,7 @@ export {
|
|
|
6095
6290
|
LegalDisclaimerEngine,
|
|
6096
6291
|
LagoTokenMeter,
|
|
6097
6292
|
KnowledgeGraphLinker,
|
|
6293
|
+
KeywordPermutatorEngine,
|
|
6098
6294
|
IsrCacheManager,
|
|
6099
6295
|
InstantMatrixSearchEngine,
|
|
6100
6296
|
IndexNowClient,
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export type SearchIntentType = "commercial" | "transactional" | "informational" | "navigational";
|
|
9
|
+
export interface GeneratedKeyword {
|
|
10
|
+
keyword: string;
|
|
11
|
+
exactMatch: string;
|
|
12
|
+
phraseMatch: string;
|
|
13
|
+
intent: SearchIntentType;
|
|
14
|
+
product: string;
|
|
15
|
+
modifier?: string;
|
|
16
|
+
location?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PermutatorOptions {
|
|
19
|
+
products: string[];
|
|
20
|
+
words?: string[];
|
|
21
|
+
locations?: string[];
|
|
22
|
+
maxCombinations?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class KeywordPermutatorEngine {
|
|
25
|
+
/**
|
|
26
|
+
* Generates a combinatorial matrix of keywords.
|
|
27
|
+
*/
|
|
28
|
+
static generateKeywordMatrix(options: PermutatorOptions): GeneratedKeyword[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
export interface ParsedUrlStructure {
|
|
9
|
+
url: string;
|
|
10
|
+
scheme: string;
|
|
11
|
+
domain: string;
|
|
12
|
+
path: string;
|
|
13
|
+
depth: number;
|
|
14
|
+
dir1?: string;
|
|
15
|
+
dir2?: string;
|
|
16
|
+
dir3?: string;
|
|
17
|
+
lastDir: string;
|
|
18
|
+
queryParams: Record<string, string>;
|
|
19
|
+
hashFragment?: string;
|
|
20
|
+
slugTokens: string[];
|
|
21
|
+
charCount: number;
|
|
22
|
+
hasTrailingSlash: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare class UrlyticsEngine {
|
|
25
|
+
/**
|
|
26
|
+
* Parses a single URL into its granular structural components.
|
|
27
|
+
*/
|
|
28
|
+
static parseUrl(rawUrl: string): ParsedUrlStructure;
|
|
29
|
+
/**
|
|
30
|
+
* Batch processes an array of URLs for comparative directory analysis.
|
|
31
|
+
*/
|
|
32
|
+
static analyzeUrls(urls: string[]): ParsedUrlStructure[];
|
|
33
|
+
}
|
package/package.json
CHANGED
package/src/engine.test.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { LegalDisclaimerEngine, MULTILINGUAL_BANNED_DISPARAGING_WORDS } from "./
|
|
|
4
4
|
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
5
5
|
import { PseoMatrixEngine } from "./matrix-engine";
|
|
6
6
|
import { getSeoAgentPrompt } from "./llm-prompt";
|
|
7
|
+
import { UrlyticsEngine } from "./urlytics-engine";
|
|
8
|
+
import { KeywordPermutatorEngine } from "./keyword-permutator";
|
|
9
|
+
import { renderBrandIconSvg } from "./brand-icons";
|
|
7
10
|
|
|
8
11
|
describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
|
|
9
12
|
it("English: Strips stop words and years", () => {
|
|
@@ -134,3 +137,53 @@ describe("Master Programmatic Matrix Engine (English Default)", () => {
|
|
|
134
137
|
expect(prompt).toContain("Schema.org");
|
|
135
138
|
});
|
|
136
139
|
});
|
|
140
|
+
|
|
141
|
+
describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
|
|
142
|
+
it("UrlyticsEngine: Parses and decomposes URL hierarchy", () => {
|
|
143
|
+
const parsed = UrlyticsEngine.parseUrl("https://acme.com/solutions/crm/fr/paris?src=seo&ref=top#pricing");
|
|
144
|
+
expect(parsed.domain).toBe("acme.com");
|
|
145
|
+
expect(parsed.depth).toBe(4);
|
|
146
|
+
expect(parsed.dir1).toBe("solutions");
|
|
147
|
+
expect(parsed.lastDir).toBe("paris");
|
|
148
|
+
expect(parsed.queryParams.src).toBe("seo");
|
|
149
|
+
expect(parsed.hashFragment).toBe("pricing");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("KeywordPermutatorEngine: Generates combinatorial SEM matrix with intent", () => {
|
|
153
|
+
const matrix = KeywordPermutatorEngine.generateKeywordMatrix({
|
|
154
|
+
products: ["crm", "billing"],
|
|
155
|
+
words: ["best", "pricing"],
|
|
156
|
+
locations: ["paris"],
|
|
157
|
+
});
|
|
158
|
+
expect(matrix.length).toBeGreaterThan(5);
|
|
159
|
+
const hasBestCrm = matrix.some((k) => k.keyword === "best crm" && k.intent === "commercial");
|
|
160
|
+
expect(hasBestCrm).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("BrandIcons: Resolves official SVG vector for Shopify, Facebook, Slack", () => {
|
|
164
|
+
const shopifySvg = renderBrandIconSvg("shopify");
|
|
165
|
+
expect(shopifySvg).toContain("<svg");
|
|
166
|
+
expect(shopifySvg).toContain("viewBox");
|
|
167
|
+
|
|
168
|
+
const fbSvg = renderBrandIconSvg("facebook");
|
|
169
|
+
expect(fbSvg).toContain("<svg");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("ExtendedSchemas: Builds AggregateRating and AggregateOffer (Santifer model)", () => {
|
|
173
|
+
const rating = ExtendedSchemaGraphBuilder.buildAggregateRating({
|
|
174
|
+
itemReviewedName: "Acme CRM",
|
|
175
|
+
ratingValue: 4.9,
|
|
176
|
+
reviewCount: 1280,
|
|
177
|
+
});
|
|
178
|
+
expect(rating["@type"]).toBe("AggregateRating");
|
|
179
|
+
expect(rating.ratingValue).toBe(4.9);
|
|
180
|
+
|
|
181
|
+
const offer = ExtendedSchemaGraphBuilder.buildAggregateOffer({
|
|
182
|
+
lowPrice: 29,
|
|
183
|
+
highPrice: 99,
|
|
184
|
+
currency: "EUR",
|
|
185
|
+
});
|
|
186
|
+
expect(offer["@type"]).toBe("AggregateOffer");
|
|
187
|
+
expect(offer.lowPrice).toBe("29");
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/extended-schemas.ts
CHANGED
|
@@ -276,4 +276,102 @@ export class ExtendedSchemaGraphBuilder {
|
|
|
276
276
|
})),
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 7. AggregateRating (Google Gold Stars in SERPs) - Inspired by santifer-irepair
|
|
282
|
+
*/
|
|
283
|
+
static buildAggregateRating(opts: {
|
|
284
|
+
ratingValue?: number;
|
|
285
|
+
reviewCount?: number;
|
|
286
|
+
bestRating?: number;
|
|
287
|
+
worstRating?: number;
|
|
288
|
+
itemReviewedName: string;
|
|
289
|
+
}): Record<string, unknown> {
|
|
290
|
+
return {
|
|
291
|
+
"@type": "AggregateRating",
|
|
292
|
+
ratingValue: opts.ratingValue ?? 4.9,
|
|
293
|
+
reviewCount: opts.reviewCount ?? 1280,
|
|
294
|
+
bestRating: opts.bestRating ?? 5,
|
|
295
|
+
worstRating: opts.worstRating ?? 1,
|
|
296
|
+
itemReviewed: {
|
|
297
|
+
"@type": "Thing",
|
|
298
|
+
name: opts.itemReviewedName,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 8. AggregateOffer (Price Ranges & Tiers) - Inspired by santifer-irepair
|
|
305
|
+
*/
|
|
306
|
+
static buildAggregateOffer(opts: {
|
|
307
|
+
lowPrice: number;
|
|
308
|
+
highPrice: number;
|
|
309
|
+
currency: string;
|
|
310
|
+
offerCount?: number;
|
|
311
|
+
description?: string;
|
|
312
|
+
}): Record<string, unknown> {
|
|
313
|
+
return {
|
|
314
|
+
"@type": "AggregateOffer",
|
|
315
|
+
lowPrice: String(opts.lowPrice),
|
|
316
|
+
highPrice: String(opts.highPrice),
|
|
317
|
+
priceCurrency: opts.currency,
|
|
318
|
+
offerCount: String(opts.offerCount ?? 3),
|
|
319
|
+
description: opts.description || "Transparent pricing with zero lock-in",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 9. Service with ServiceArea & Providers
|
|
325
|
+
*/
|
|
326
|
+
static buildService(opts: {
|
|
327
|
+
name: string;
|
|
328
|
+
description: string;
|
|
329
|
+
providerName: string;
|
|
330
|
+
areaServed?: string[];
|
|
331
|
+
serviceType?: string;
|
|
332
|
+
}): Record<string, unknown> {
|
|
333
|
+
return {
|
|
334
|
+
"@context": "https://schema.org",
|
|
335
|
+
"@type": "Service",
|
|
336
|
+
name: opts.name,
|
|
337
|
+
description: opts.description,
|
|
338
|
+
serviceType: opts.serviceType || "SoftwareService",
|
|
339
|
+
provider: {
|
|
340
|
+
"@type": "Organization",
|
|
341
|
+
name: opts.providerName,
|
|
342
|
+
},
|
|
343
|
+
areaServed: opts.areaServed
|
|
344
|
+
? opts.areaServed.map((city) => ({
|
|
345
|
+
"@type": "City",
|
|
346
|
+
name: city,
|
|
347
|
+
}))
|
|
348
|
+
: undefined,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 10. WebSite with Google Sitelinks SearchBox
|
|
354
|
+
*/
|
|
355
|
+
static buildWebSite(opts: {
|
|
356
|
+
name: string;
|
|
357
|
+
url: string;
|
|
358
|
+
searchUrlTemplate?: string;
|
|
359
|
+
}): Record<string, unknown> {
|
|
360
|
+
return {
|
|
361
|
+
"@context": "https://schema.org",
|
|
362
|
+
"@type": "WebSite",
|
|
363
|
+
name: opts.name,
|
|
364
|
+
url: opts.url,
|
|
365
|
+
potentialAction: opts.searchUrlTemplate
|
|
366
|
+
? {
|
|
367
|
+
"@type": "SearchAction",
|
|
368
|
+
target: {
|
|
369
|
+
"@type": "EntryPoint",
|
|
370
|
+
urlTemplate: opts.searchUrlTemplate,
|
|
371
|
+
},
|
|
372
|
+
"query-input": "required name=search_term_string",
|
|
373
|
+
}
|
|
374
|
+
: undefined,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
279
377
|
}
|
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
|
+
}
|
|
@@ -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
|
+
}
|