@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.
@@ -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
@@ -3214,7 +3272,7 @@ class PseoMatrixEngine {
3214
3272
  const countryCode = cleanSeoSlug(loc.country, { language: lang });
3215
3273
  const citySlug = cleanSeoSlug(loc.slug, { language: lang });
3216
3274
  const serviceSlug = cleanSeoSlug(s.slug, { language: lang });
3217
- const urlPath = `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
3275
+ const urlPath = options.cleanDirectRoutes !== false ? `/${serviceSlug}/${citySlug}` : `${prefix}/${serviceSlug}/${countryCode}/${citySlug}`;
3218
3276
  const fullUrl = `${cleanDomain}${urlPath}`;
3219
3277
  const isIndexed = (loc.population ?? 20000) >= minPop;
3220
3278
  const robots = isIndexed ? "index, follow" : "noindex, follow";
@@ -3637,6 +3695,40 @@ class PseoMatrixEngine {
3637
3695
  }
3638
3696
  return allPages;
3639
3697
  }
3698
+ resolvePage(slugOrPath, domain, data, options) {
3699
+ const path = typeof slugOrPath === "string" ? slugOrPath.startsWith("/") ? slugOrPath : `/${slugOrPath}` : `/${slugOrPath.join("/")}`;
3700
+ const pages = this.generateAllMatrices(domain, data, options);
3701
+ return pages.find((p) => p.urlPath === path);
3702
+ }
3703
+ generateSitemapXml(domain, data, options) {
3704
+ const pages = this.generateAllMatrices(domain, data, options).filter((p) => p.robots.includes("index"));
3705
+ const now = new Date().toISOString();
3706
+ const urls = pages.map((p) => ` <url>
3707
+ <loc>${p.canonicalUrl}</loc>
3708
+ <lastmod>${now}</lastmod>
3709
+ <changefreq>weekly</changefreq>
3710
+ <priority>0.8</priority>
3711
+ </url>`).join(`
3712
+ `);
3713
+ return `<?xml version="1.0" encoding="UTF-8"?>
3714
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3715
+ ${urls}
3716
+ </urlset>`;
3717
+ }
3718
+ generateLlmsTxt(domain, data, options) {
3719
+ const pages = this.generateAllMatrices(domain, data, options);
3720
+ const lines = [
3721
+ `# ${options.brandName} — AI Knowledge Graph & Page Hub`,
3722
+ `> Complete index of services, tools, integrations, and local solution hubs.`,
3723
+ "",
3724
+ `## Indexed Programmatic Hubs (${pages.length} Pages)`
3725
+ ];
3726
+ for (const p of pages) {
3727
+ lines.push(`- [${p.h1}](${p.canonicalUrl}): ${p.description}`);
3728
+ }
3729
+ return lines.join(`
3730
+ `);
3731
+ }
3640
3732
  }
3641
3733
  // src/brand-icons.ts
3642
3734
  var BRAND_ICONS = {
@@ -3754,6 +3846,144 @@ function renderBrandIconSvg(input, className = "w-5 h-5 inline-block") {
3754
3846
  return "";
3755
3847
  return `<svg class="${className}" viewBox="${icon.viewBox}" fill="currentColor" aria-hidden="true"><path d="${icon.svgPath}"/></svg>`;
3756
3848
  }
3849
+ // src/urlytics-engine.ts
3850
+ class UrlyticsEngine {
3851
+ static parseUrl(rawUrl) {
3852
+ if (!rawUrl || typeof rawUrl !== "string") {
3853
+ return {
3854
+ url: "",
3855
+ scheme: "",
3856
+ domain: "",
3857
+ path: "/",
3858
+ depth: 0,
3859
+ lastDir: "",
3860
+ queryParams: {},
3861
+ slugTokens: [],
3862
+ charCount: 0,
3863
+ hasTrailingSlash: false
3864
+ };
3865
+ }
3866
+ try {
3867
+ const parsed = new URL(rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`);
3868
+ const pathClean = parsed.pathname.replace(/\/+$/, "");
3869
+ const segments = pathClean.split("/").filter(Boolean);
3870
+ const queryParams = {};
3871
+ parsed.searchParams.forEach((val, key) => {
3872
+ queryParams[key] = val;
3873
+ });
3874
+ const lastDir = segments.length > 0 ? segments[segments.length - 1] : "";
3875
+ const slugTokens = lastDir.split("-").filter(Boolean);
3876
+ return {
3877
+ url: rawUrl,
3878
+ scheme: parsed.protocol.replace(":", ""),
3879
+ domain: parsed.hostname,
3880
+ path: parsed.pathname,
3881
+ depth: segments.length,
3882
+ dir1: segments[0],
3883
+ dir2: segments[1],
3884
+ dir3: segments[2],
3885
+ lastDir,
3886
+ queryParams,
3887
+ hashFragment: parsed.hash ? parsed.hash.replace("#", "") : undefined,
3888
+ slugTokens,
3889
+ charCount: rawUrl.length,
3890
+ hasTrailingSlash: parsed.pathname.length > 1 && parsed.pathname.endsWith("/")
3891
+ };
3892
+ } catch {
3893
+ return {
3894
+ url: rawUrl,
3895
+ scheme: "unknown",
3896
+ domain: "",
3897
+ path: rawUrl,
3898
+ depth: 0,
3899
+ lastDir: rawUrl,
3900
+ queryParams: {},
3901
+ slugTokens: [rawUrl],
3902
+ charCount: rawUrl.length,
3903
+ hasTrailingSlash: false
3904
+ };
3905
+ }
3906
+ }
3907
+ static analyzeUrls(urls) {
3908
+ return urls.map((u) => this.parseUrl(u));
3909
+ }
3910
+ }
3911
+ // src/keyword-permutator.ts
3912
+ class KeywordPermutatorEngine {
3913
+ static generateKeywordMatrix(options) {
3914
+ const { products, words = [], locations = [], maxCombinations = 5000 } = options;
3915
+ const results = [];
3916
+ const intentKeywords = {
3917
+ transactional: ["buy", "pricing", "cost", "hire", "quote", "tarif", "prix", "devis", "acheter"],
3918
+ commercial: ["best", "top", "review", "vs", "comparison", "alternative", "comparatif", "meilleur"],
3919
+ informational: ["how to", "what is", "guide", "tutorial", "definition", "comment", "quest ce que"],
3920
+ navigational: ["login", "app", "portal", "website", "connexion"]
3921
+ };
3922
+ const detectIntent = (text) => {
3923
+ const lower = text.toLowerCase();
3924
+ for (const [intent, triggers] of Object.entries(intentKeywords)) {
3925
+ if (triggers.some((t) => lower.includes(t))) {
3926
+ return intent;
3927
+ }
3928
+ }
3929
+ return "commercial";
3930
+ };
3931
+ for (const prod of products) {
3932
+ results.push({
3933
+ keyword: prod,
3934
+ exactMatch: `[${prod}]`,
3935
+ phraseMatch: `"${prod}"`,
3936
+ intent: detectIntent(prod),
3937
+ product: prod
3938
+ });
3939
+ for (const word of words) {
3940
+ const kw1 = `${word} ${prod}`;
3941
+ const kw2 = `${prod} ${word}`;
3942
+ results.push({
3943
+ keyword: kw1,
3944
+ exactMatch: `[${kw1}]`,
3945
+ phraseMatch: `"${kw1}"`,
3946
+ intent: detectIntent(word),
3947
+ product: prod,
3948
+ modifier: word
3949
+ });
3950
+ results.push({
3951
+ keyword: kw2,
3952
+ exactMatch: `[${kw2}]`,
3953
+ phraseMatch: `"${kw2}"`,
3954
+ intent: detectIntent(word),
3955
+ product: prod,
3956
+ modifier: word
3957
+ });
3958
+ for (const loc of locations) {
3959
+ const kwLoc1 = `${word} ${prod} ${loc}`;
3960
+ const kwLoc2 = `${prod} ${loc} ${word}`;
3961
+ results.push({
3962
+ keyword: kwLoc1,
3963
+ exactMatch: `[${kwLoc1}]`,
3964
+ phraseMatch: `"${kwLoc1}"`,
3965
+ intent: detectIntent(word),
3966
+ product: prod,
3967
+ modifier: word,
3968
+ location: loc
3969
+ });
3970
+ results.push({
3971
+ keyword: kwLoc2,
3972
+ exactMatch: `[${kwLoc2}]`,
3973
+ phraseMatch: `"${kwLoc2}"`,
3974
+ intent: detectIntent(word),
3975
+ product: prod,
3976
+ modifier: word,
3977
+ location: loc
3978
+ });
3979
+ if (results.length >= maxCombinations)
3980
+ return results;
3981
+ }
3982
+ }
3983
+ }
3984
+ return results;
3985
+ }
3986
+ }
3757
3987
  // src/llm-prompt.ts
3758
3988
  var PSEO_AGENT_SYSTEM_PROMPT = `
3759
3989
  You are the Programmatic SEO Implementation Specialist powered by @lynxflow/seo-engine.
@@ -3788,7 +4018,21 @@ When discovering product modules, features, or services to build the SEO matrice
3788
4018
 
3789
4019
  ---
3790
4020
 
3791
- ### \uD83C\uDFC6 3. THE ARCHITECTURAL PLAYBOOK OF SAAS TITANS (7M+ PAGES WITH 0 DB BOTTLENECKS)
4021
+ ### \uD83D\uDCCF 3. THE GOLDEN RULES OF URL ARCHITECTURE (MAX 3-4 SEGMENTS, ZERO PARASITE WORDS)
4022
+
4023
+ - **Rule A (2 to 3 URL Segments Ideal, 4 Maximum):**
4024
+ - ✅ **2 Segments (Top SEO Performance):** \`site.com/{service}/{city}\` (e.g. \`/autopost-facebook/lyon\`, \`/crm/paris\`).
4025
+ - ✅ **3 Segments (B2B Persona/Hub):** \`site.com/for/{target}/{city}\` or \`site.com/{locale}/{service}/{city}\`.
4026
+ - ❌ **Never 5 to 6 Segments:** Avoid \`/solutions/ai/autopost/facebook/fr/lyon\`.
4027
+ - **Rule B (Eliminate All Parasite Noise Words):**
4028
+ - Banish generic wrapper words like \`/solutions/\`, \`/pages/\`, \`/items/\`. Go direct to user search intent: What you do + Platform + Location.
4029
+ - **Rule C (The SDK Does 100% of the Heavy Lifting):**
4030
+ - The developer/AI only registers 3-4 product features and target cities in \`lib/seo.ts\`.
4031
+ - 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.
4032
+
4033
+ ---
4034
+
4035
+ ### \uD83C\uDFC6 4. THE ARCHITECTURAL PLAYBOOK OF SAAS TITANS (7M+ PAGES WITH 0 DB BOTTLENECKS)
3792
4036
 
3793
4037
  How category leaders generate millions of high-converting pages without server saturation:
3794
4038
 
@@ -3800,18 +4044,18 @@ How category leaders generate millions of high-converting pages without server s
3800
4044
 
3801
4045
  2. **The 4 Pillars of Zero-Latency (< 0.05ms) Resolution:**
3802
4046
  - **0 Bytes in Database:** Never generate physical static files or query heavy databases per request. Pages are resolved mathematically in RAM upon arrival.
3803
- - **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\` (or \`app/[locale]/[...slug]/page.tsx\`) handles all matrices through one lightweight layout.
3804
- - **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge (Cloudflare / Vercel Edge) on first request.
4047
+ - **1 Single Universal Catch-All Route:** \`app/[...slug]/page.tsx\` handles all matrices through one lightweight layout.
4048
+ - **Edge Caching & ISR:** The rendered HTML is instantly cached at the CDN/Edge on first request.
3805
4049
  - **Automatic Siloing & Mesh Linking:**
3806
4050
  - Links to 3 neighboring geographic cities for the same service.
3807
4051
  - Links to 4 related professions/industries in the same target category.
3808
4052
  - Complete Schema.org BreadcrumbList navigation.
3809
- - **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index to respect Google's crawl budget.
4053
+ - **Sharded XML Sitemaps:** Split into 50,000 URL chunks with a master sitemap index.
3810
4054
  - **Dynamic \`/llms.txt\` Feed:** High-density Markdown stream enabling direct indexing by ChatGPT Search & Perplexity.
3811
4055
 
3812
4056
  ---
3813
4057
 
3814
- ### \uD83D\uDEE0️ 4. AI AGENT IMPLEMENTATION PROTOCOL (STEP-BY-STEP)
4058
+ ### \uD83D\uDEE0️ 5. AI AGENT IMPLEMENTATION PROTOCOL (STEP-BY-STEP)
3815
4059
 
3816
4060
  When setting up programmatic SEO in any codebase, the AI Agent MUST generate these exact 4 files:
3817
4061
 
@@ -3824,6 +4068,7 @@ export const matrixEngine = new PseoMatrixEngine();
3824
4068
  export const SEO_CONFIG = {
3825
4069
  brandName: "Acme",
3826
4070
  language: "en",
4071
+ cleanDirectRoutes: true, // Eliminates parasite words (/solutions/, etc.)
3827
4072
  minPopulationToIndex: 15_000,
3828
4073
  };
3829
4074
 
@@ -3831,19 +4076,23 @@ export const SEO_CONFIG = {
3831
4076
  export const PSEO_DATASET = {
3832
4077
  services: [
3833
4078
  // 1 per major frontend feature branch
3834
- { slug: "crm-pipeline", name: "Visual CRM Pipeline", category: "Sales", keyFeatures: ["Kanban", "Lead scoring"], description: "Visual deal tracking" },
4079
+ { slug: "autopost-facebook", name: "Autopost Facebook & Instagram", category: "Social Media", keyFeatures: ["AI Scheduling", "Visual Calendar", "Auto-Hashtags"], description: "Automated social media posting engine" },
4080
+ ],
4081
+ locations: [
4082
+ { slug: "paris", name: "Paris", region: "Île-de-France", country: "France", population: 2_161_000, currency: "EUR", currencySymbol: "€" },
4083
+ { slug: "lyon", name: "Lyon", region: "Rhône-Alpes", country: "France", population: 522_000, currency: "EUR", currencySymbol: "€" },
3835
4084
  ],
3836
4085
  targets: [
3837
- { slug: "law-firms", name: "Law Firms", type: "industry", painPoints: ["Billing"], benefits: ["Compliance"] },
4086
+ { slug: "agences", name: "Marketing Agencies", type: "industry", painPoints: ["Manual Posting"], benefits: ["10x Output"] },
3838
4087
  ],
3839
4088
  integrations: [
3840
- { slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Order sync"] },
4089
+ { slug: "shopify", name: "Shopify", category: "E-Commerce", syncFeatures: ["Catalog sync"] },
3841
4090
  ],
3842
4091
  templates: [
3843
- { slug: "sales-tracker-excel", title: "Sales Tracker", topic: "Sales", format: "excel", benefits: ["Instant setup"] },
4092
+ { slug: "social-calendar-excel", title: "Social Media Calendar", topic: "Social Media", format: "excel", benefits: ["Instant setup"] },
3844
4093
  ],
3845
4094
  glossaryTerms: [
3846
- { slug: "mrr", term: "MRR", shortDefinition: "Monthly Recurring Revenue" },
4095
+ { slug: "engagement-rate", term: "Engagement Rate", shortDefinition: "Percentage of audience interacting with content" },
3847
4096
  ],
3848
4097
  };
3849
4098
  \`\`\`
@@ -3856,19 +4105,15 @@ import { matrixEngine, SEO_CONFIG, PSEO_DATASET } from "@/lib/seo";
3856
4105
  import { renderBrandIconSvg } from "@lynxflow/seo-engine";
3857
4106
 
3858
4107
  export async function generateMetadata({ params }: { params: { slug: string[] } }) {
3859
- const path = "/" + params.slug.join("/");
3860
4108
  const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
3861
- const pages = matrixEngine.generateAllMatrices(domain, PSEO_DATASET, SEO_CONFIG);
3862
- const page = pages.find((p) => p.urlPath === path);
4109
+ const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
3863
4110
  if (!page) return {};
3864
4111
  return { title: page.title, description: page.description, alternates: { canonical: page.canonicalUrl }, robots: page.robots };
3865
4112
  }
3866
4113
 
3867
4114
  export default async function ProgrammaticPage({ params }: { params: { slug: string[] } }) {
3868
- const path = "/" + params.slug.join("/");
3869
4115
  const domain = process.env.NEXT_PUBLIC_SITE_URL || "https://example.com";
3870
- const pages = matrixEngine.generateAllMatrices(domain, PSEO_DATASET, SEO_CONFIG);
3871
- const page = pages.find((p) => p.urlPath === path);
4116
+ const page = matrixEngine.resolvePage(params.slug, domain, PSEO_DATASET, SEO_CONFIG);
3872
4117
  if (!page) notFound();
3873
4118
 
3874
4119
  // Automatic Brand Icon Detection