@lynxflow/seo-engine 1.5.4 → 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.
@@ -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
+ });
@@ -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
@@ -14,6 +14,9 @@ export * from "./engine";
14
14
  export * from "./slug-engine";
15
15
  export * from "./legal-disclaimers";
16
16
  export * from "./matrix-engine";
17
+ export * from "./brand-icons";
18
+ export * from "./urlytics-engine";
19
+ export * from "./keyword-permutator";
17
20
  export * from "./llm-prompt";
18
21
  export * from "./auth-key";
19
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
@@ -97,10 +97,12 @@ export const PSEO_DATASET = {
97
97
  };
98
98
  \`\`\`
99
99
 
100
- #### File 2: Universal Dynamic Catch-All Route (\`app/[...slug]/page.tsx\`)
100
+ #### File 2: Ultra-Modern Dynamic Catch-All Route (\`app/[...slug]/page.tsx\`)
101
101
  \`\`\`tsx
102
102
  import { notFound } from "next/navigation";
103
+ import Link from "next/link";
103
104
  import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
105
+ import { renderBrandIconSvg } from "@lynxflow/seo-engine";
104
106
 
105
107
  export async function generateMetadata({ params }: { params: { slug: string[] } }) {
106
108
  const path = "/" + params.slug.join("/");
@@ -118,12 +120,75 @@ export default async function ProgrammaticPage({ params }: { params: { slug: str
118
120
  const page = pages.find((p) => p.urlPath === path);
119
121
  if (!page) notFound();
120
122
 
123
+ // Automatic Brand Icon Detection
124
+ const brandSvg = renderBrandIconSvg(page.integration?.name || page.competitor?.name || page.service?.name || "");
125
+
121
126
  return (
122
- <article className="max-w-4xl mx-auto py-12 px-6">
127
+ <article className="min-h-screen bg-background text-foreground transition-colors duration-200">
128
+ {/* 1. Official Google Schema.org JSON-LD */}
123
129
  <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(page.schemaGraph) }} />
124
- <h1 className="text-4xl font-extrabold">{page.h1}</h1>
125
- <p className="mt-4 text-xl text-gray-600">{page.description}</p>
126
- {page.disclaimerText && <aside className="mt-8 p-4 bg-gray-50 border rounded text-xs text-gray-500">{page.disclaimerText}</aside>}
130
+
131
+ <main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16 space-y-12">
132
+ {/* 2. Hero Section with Glassmorphism Badge */}
133
+ <header className="space-y-6 text-center md:text-left">
134
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-semibold bg-primary/10 text-primary border border-primary/20 backdrop-blur-md">
135
+ {brandSvg && <span dangerouslySetInnerHTML={{ __html: brandSvg }} className="w-4 h-4 flex items-center" />}
136
+ <span>{page.matrixFamily.toUpperCase()}</span>
137
+ </div>
138
+
139
+ <h1 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight leading-tight">
140
+ {page.h1}
141
+ </h1>
142
+ <p className="text-lg sm:text-xl text-muted-foreground max-w-3xl leading-relaxed">
143
+ {page.description}
144
+ </p>
145
+ </header>
146
+
147
+ {/* 3. AEO / GEO Direct-Answer Callout Box (Optimized for ChatGPT & Perplexity Citations) */}
148
+ <section className="p-6 md:p-8 rounded-2xl bg-card border border-border shadow-sm space-y-3" data-geo-extract="true">
149
+ <div className="flex items-center gap-2 text-primary font-bold text-sm">
150
+ <span>⚡</span>
151
+ <span>Direct Summary & Key Takeaways</span>
152
+ </div>
153
+ <p className="text-base text-card-foreground leading-relaxed">
154
+ {page.description}
155
+ </p>
156
+ </section>
157
+
158
+ {/* 4. Interactive Value & Feature Grid */}
159
+ <section className="grid grid-cols-1 md:grid-cols-3 gap-6">
160
+ {(page.service?.keyFeatures || ["Real-time Sync", "Bank-grade Security", "Zero Lock-in"]).map((feat, idx) => (
161
+ <div key={idx} className="p-6 rounded-xl bg-card/60 border border-border/80 hover:border-primary/50 transition duration-200 space-y-2">
162
+ <div className="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center font-bold text-sm">
163
+ 0{idx + 1}
164
+ </div>
165
+ <h3 className="font-bold text-lg">{feat}</h3>
166
+ <p className="text-sm text-muted-foreground">Automated workflows engineered for high-performance scale.</p>
167
+ </div>
168
+ ))}
169
+ </section>
170
+
171
+ {/* 5. Geodesic Mesh Linking Footer (Prevents Orphan Pages & Spreads PageRank) */}
172
+ {page.neighboringLinks && page.neighboringLinks.length > 0 && (
173
+ <nav className="pt-8 border-t border-border space-y-4">
174
+ <h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Nearby & Related Hubs</h3>
175
+ <div className="flex flex-wrap gap-2">
176
+ {page.neighboringLinks.map((link, idx) => (
177
+ <Link key={idx} href={link.url} className="px-3 py-1.5 rounded-lg text-xs font-medium bg-secondary text-secondary-foreground hover:bg-primary hover:text-primary-foreground transition duration-150">
178
+ {link.name}
179
+ </Link>
180
+ ))}
181
+ </div>
182
+ </nav>
183
+ )}
184
+
185
+ {/* 6. SeedRank Legal Compliance Notice */}
186
+ {page.disclaimerText && (
187
+ <aside className="p-4 rounded-lg bg-muted/40 border border-border text-xs text-muted-foreground leading-relaxed">
188
+ {page.disclaimerText}
189
+ </aside>
190
+ )}
191
+ </main>
127
192
  </article>
128
193
  );
129
194
  }
@@ -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
+ }