@lynxflow/seo-engine 1.5.9 → 1.6.0

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 🌐 Dynamic i18n & Locale Auto-Detector
3
+ * Automatically detects and synchronizes website locales without requiring hardcoded lists.
4
+ * Compatible with next-intl, next-i18next, i18next, Paraglide, and Astro i18n.
5
+ */
6
+ export declare const SUPPORTED_CANONICAL_LOCALES: readonly ["en", "fr", "es", "de", "it", "pt", "nl", "ru", "sv", "pl", "ja", "zh", "ar"];
7
+ export type SupportedLocale = (typeof SUPPORTED_CANONICAL_LOCALES)[number];
8
+ export declare class I18nDetector {
9
+ /**
10
+ * Resolves a clean 2-letter ISO language code from any locale string (e.g. "fr-FR" -> "fr").
11
+ */
12
+ static normalizeLocale(localeInput?: string): string;
13
+ /**
14
+ * Auto-detects and extracts active locales from common environment variables or array inputs.
15
+ */
16
+ static resolveActiveLocales(explicitLocales?: string[] | string): string[];
17
+ /**
18
+ * Matches whether a given path segment is a language prefix (e.g. "/fr/autopost-facebook/paris").
19
+ */
20
+ static extractLocaleFromPath(pathname: string, activeLocales: string[]): {
21
+ locale: string;
22
+ remainingPath: string;
23
+ };
24
+ }
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./slug-engine";
14
14
  export * from "./legal-disclaimers";
15
15
  export * from "./matrix-engine";
16
16
  export * from "./built-in-locations";
17
+ export * from "./i18n-detector";
17
18
  export * from "./brand-icons";
18
19
  export * from "./urlytics-engine";
19
20
  export * from "./keyword-permutator";
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ __export(exports_src, {
61
61
  SerpClient: () => SerpClient,
62
62
  SeoOpportunitiesDecayDetector: () => SeoOpportunitiesDecayDetector,
63
63
  SchemaGraphBuilder: () => SchemaGraphBuilder,
64
+ SUPPORTED_CANONICAL_LOCALES: () => SUPPORTED_CANONICAL_LOCALES,
64
65
  SCHEMA_LOCAL_BUSINESS_MAP: () => SCHEMA_LOCAL_BUSINESS_MAP,
65
66
  RssSyndicationFeedGenerator: () => RssSyndicationFeedGenerator,
66
67
  RealReviewsSyncEngine: () => RealReviewsSyncEngine,
@@ -84,6 +85,7 @@ __export(exports_src, {
84
85
  IsrCacheManager: () => IsrCacheManager,
85
86
  InstantMatrixSearchEngine: () => InstantMatrixSearchEngine,
86
87
  IndexNowClient: () => IndexNowClient,
88
+ I18nDetector: () => I18nDetector,
87
89
  HyperswitchGateway: () => HyperswitchGateway,
88
90
  GeoMeshLinkingEngine: () => GeoMeshLinkingEngine,
89
91
  ExtendedSchemaGraphBuilder: () => ExtendedSchemaGraphBuilder,
@@ -3862,6 +3864,11 @@ class PseoMatrixEngine {
3862
3864
  ${urls}
3863
3865
  </urlset>`;
3864
3866
  }
3867
+ resolveServicePage(serviceSlug, citySlug, domain, data, options) {
3868
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
3869
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
3870
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
3871
+ }
3865
3872
  generateLlmsTxt(domain, data, options) {
3866
3873
  const pages = this.generateAllMatrices(domain, data, options);
3867
3874
  const lines = [
@@ -3876,6 +3883,91 @@ ${urls}
3876
3883
  return lines.join(`
3877
3884
  `);
3878
3885
  }
3886
+ generateRobotsTxt(domain, options) {
3887
+ const cleanDomain = domain.replace(/\/+$/, "");
3888
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
3889
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
3890
+ const lines = [
3891
+ "User-agent: *",
3892
+ "Allow: /",
3893
+ ...disallows.map((d) => `Disallow: ${d}`),
3894
+ ""
3895
+ ];
3896
+ if (options?.allowAiBots !== false) {
3897
+ lines.push("# AI Search Crawlers Optimization (AEO/GEO)", "User-agent: GPTBot", "Allow: /", "", "User-agent: ClaudeBot", "Allow: /", "", "User-agent: PerplexityBot", "Allow: /", "", "User-agent: Applebot", "Allow: /", "");
3898
+ }
3899
+ lines.push(`Sitemap: ${sitemap}`);
3900
+ return lines.join(`
3901
+ `);
3902
+ }
3903
+ getTelemetryPayload(domain, data, options) {
3904
+ const pages = this.generateAllMatrices(domain, data, options);
3905
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
3906
+ const matrixBreakdown = {};
3907
+ for (const p of pages) {
3908
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
3909
+ }
3910
+ return {
3911
+ domain,
3912
+ brandName: options.brandName,
3913
+ language: options.language || "en",
3914
+ languages: options.languages || [options.language || "en"],
3915
+ totalPages: pages.length,
3916
+ indexedPages: indexedPages.length,
3917
+ servicesCount: data.services?.length ?? 0,
3918
+ matrixBreakdown,
3919
+ timestamp: new Date().toISOString()
3920
+ };
3921
+ }
3922
+ }
3923
+ // src/i18n-detector.ts
3924
+ var SUPPORTED_CANONICAL_LOCALES = [
3925
+ "en",
3926
+ "fr",
3927
+ "es",
3928
+ "de",
3929
+ "it",
3930
+ "pt",
3931
+ "nl",
3932
+ "ru",
3933
+ "sv",
3934
+ "pl",
3935
+ "ja",
3936
+ "zh",
3937
+ "ar"
3938
+ ];
3939
+
3940
+ class I18nDetector {
3941
+ static normalizeLocale(localeInput) {
3942
+ if (!localeInput || typeof localeInput !== "string")
3943
+ return "en";
3944
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
3945
+ return clean || "en";
3946
+ }
3947
+ static resolveActiveLocales(explicitLocales) {
3948
+ if (explicitLocales) {
3949
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
3950
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
3951
+ return Array.from(new Set(normalized));
3952
+ }
3953
+ return ["en", "fr"];
3954
+ }
3955
+ static extractLocaleFromPath(pathname, activeLocales) {
3956
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
3957
+ const segments = cleanPath.split("/").filter(Boolean);
3958
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
3959
+ const locale = segments[0].toLowerCase();
3960
+ const remainingSegments = segments.slice(1);
3961
+ return {
3962
+ locale,
3963
+ remainingPath: `/${remainingSegments.join("/")}`
3964
+ };
3965
+ }
3966
+ return {
3967
+ locale: activeLocales[0] || "en",
3968
+ remainingPath: cleanPath
3969
+ };
3970
+ }
3879
3971
  }
3880
3972
  // src/brand-icons.ts
3881
3973
  var BRAND_ICONS = {
package/dist/index.mjs CHANGED
@@ -3757,6 +3757,11 @@ class PseoMatrixEngine {
3757
3757
  ${urls}
3758
3758
  </urlset>`;
3759
3759
  }
3760
+ resolveServicePage(serviceSlug, citySlug, domain, data, options) {
3761
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
3762
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
3763
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
3764
+ }
3760
3765
  generateLlmsTxt(domain, data, options) {
3761
3766
  const pages = this.generateAllMatrices(domain, data, options);
3762
3767
  const lines = [
@@ -3771,6 +3776,91 @@ ${urls}
3771
3776
  return lines.join(`
3772
3777
  `);
3773
3778
  }
3779
+ generateRobotsTxt(domain, options) {
3780
+ const cleanDomain = domain.replace(/\/+$/, "");
3781
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
3782
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
3783
+ const lines = [
3784
+ "User-agent: *",
3785
+ "Allow: /",
3786
+ ...disallows.map((d) => `Disallow: ${d}`),
3787
+ ""
3788
+ ];
3789
+ if (options?.allowAiBots !== false) {
3790
+ lines.push("# AI Search Crawlers Optimization (AEO/GEO)", "User-agent: GPTBot", "Allow: /", "", "User-agent: ClaudeBot", "Allow: /", "", "User-agent: PerplexityBot", "Allow: /", "", "User-agent: Applebot", "Allow: /", "");
3791
+ }
3792
+ lines.push(`Sitemap: ${sitemap}`);
3793
+ return lines.join(`
3794
+ `);
3795
+ }
3796
+ getTelemetryPayload(domain, data, options) {
3797
+ const pages = this.generateAllMatrices(domain, data, options);
3798
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
3799
+ const matrixBreakdown = {};
3800
+ for (const p of pages) {
3801
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
3802
+ }
3803
+ return {
3804
+ domain,
3805
+ brandName: options.brandName,
3806
+ language: options.language || "en",
3807
+ languages: options.languages || [options.language || "en"],
3808
+ totalPages: pages.length,
3809
+ indexedPages: indexedPages.length,
3810
+ servicesCount: data.services?.length ?? 0,
3811
+ matrixBreakdown,
3812
+ timestamp: new Date().toISOString()
3813
+ };
3814
+ }
3815
+ }
3816
+ // src/i18n-detector.ts
3817
+ var SUPPORTED_CANONICAL_LOCALES = [
3818
+ "en",
3819
+ "fr",
3820
+ "es",
3821
+ "de",
3822
+ "it",
3823
+ "pt",
3824
+ "nl",
3825
+ "ru",
3826
+ "sv",
3827
+ "pl",
3828
+ "ja",
3829
+ "zh",
3830
+ "ar"
3831
+ ];
3832
+
3833
+ class I18nDetector {
3834
+ static normalizeLocale(localeInput) {
3835
+ if (!localeInput || typeof localeInput !== "string")
3836
+ return "en";
3837
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
3838
+ return clean || "en";
3839
+ }
3840
+ static resolveActiveLocales(explicitLocales) {
3841
+ if (explicitLocales) {
3842
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
3843
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
3844
+ return Array.from(new Set(normalized));
3845
+ }
3846
+ return ["en", "fr"];
3847
+ }
3848
+ static extractLocaleFromPath(pathname, activeLocales) {
3849
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
3850
+ const segments = cleanPath.split("/").filter(Boolean);
3851
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
3852
+ const locale = segments[0].toLowerCase();
3853
+ const remainingSegments = segments.slice(1);
3854
+ return {
3855
+ locale,
3856
+ remainingPath: `/${remainingSegments.join("/")}`
3857
+ };
3858
+ }
3859
+ return {
3860
+ locale: activeLocales[0] || "en",
3861
+ remainingPath: cleanPath
3862
+ };
3863
+ }
3774
3864
  }
3775
3865
  // src/brand-icons.ts
3776
3866
  var BRAND_ICONS = {
@@ -6464,6 +6554,7 @@ export {
6464
6554
  SerpClient,
6465
6555
  SeoOpportunitiesDecayDetector,
6466
6556
  SchemaGraphBuilder,
6557
+ SUPPORTED_CANONICAL_LOCALES,
6467
6558
  SCHEMA_LOCAL_BUSINESS_MAP,
6468
6559
  RssSyndicationFeedGenerator,
6469
6560
  RealReviewsSyncEngine,
@@ -6487,6 +6578,7 @@ export {
6487
6578
  IsrCacheManager,
6488
6579
  InstantMatrixSearchEngine,
6489
6580
  IndexNowClient,
6581
+ I18nDetector,
6490
6582
  HyperswitchGateway,
6491
6583
  GeoMeshLinkingEngine,
6492
6584
  ExtendedSchemaGraphBuilder,
@@ -221,8 +221,35 @@ export declare class PseoMatrixEngine {
221
221
  * Generates a fully formatted XML Sitemap string.
222
222
  */
223
223
  generateSitemapXml(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
224
+ /**
225
+ * Resolves a page for a specific module/service and city.
226
+ * Useful when organizing code as app/[service]/[city]/page.tsx
227
+ */
228
+ resolveServicePage(serviceSlug: string, citySlug: string, domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): GeneratedPageMeta | undefined;
224
229
  /**
225
230
  * Generates an official /llms.txt Markdown directory for AI search bots.
226
231
  */
227
232
  generateLlmsTxt(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): string;
233
+ /**
234
+ * Generates a modern robots.txt allowing search engines and AI agents with sitemap link.
235
+ */
236
+ generateRobotsTxt(domain: string, options?: {
237
+ sitemapUrl?: string;
238
+ allowAiBots?: boolean;
239
+ disallowPaths?: string[];
240
+ }): string;
241
+ /**
242
+ * Computes telemetry & page metering data for syncing with LynxSEO Studio dashboard.
243
+ */
244
+ getTelemetryPayload(domain: string, data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1], options: MatrixOptions): {
245
+ domain: string;
246
+ brandName: string;
247
+ language: string;
248
+ languages: string[];
249
+ totalPages: number;
250
+ indexedPages: number;
251
+ servicesCount: number;
252
+ matrixBreakdown: Record<string, number>;
253
+ timestamp: string;
254
+ };
228
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.5.9",
3
+ "version": "1.6.0",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -7,6 +7,7 @@ import { getSeoAgentPrompt } from "./llm-prompt";
7
7
  import { UrlyticsEngine } from "./urlytics-engine";
8
8
  import { KeywordPermutatorEngine } from "./keyword-permutator";
9
9
  import { renderBrandIconSvg } from "./brand-icons";
10
+ import { I18nDetector } from "./i18n-detector";
10
11
 
11
12
  describe("Multilingual SEO Slug Engine (10+ Languages)", () => {
12
13
  it("English: Strips stop words and years", () => {
@@ -234,4 +235,34 @@ describe("Audited Reference Engines (Advertools, Santifer, Seonaut)", () => {
234
235
  expect(paths).toContain("/autopost-facebook/madrid");
235
236
  expect(paths).toContain("/autopost-facebook/barcelona");
236
237
  });
238
+
239
+ it("I18nDetector: Normalizes locales and extracts locale from pathname", () => {
240
+ expect(I18nDetector.normalizeLocale("fr-FR")).toBe("fr");
241
+ expect(I18nDetector.normalizeLocale("en_US")).toBe("en");
242
+
243
+ const extraction = I18nDetector.extractLocaleFromPath("/fr/autopost-facebook/paris", ["fr", "en", "es"]);
244
+ expect(extraction.locale).toBe("fr");
245
+ expect(extraction.remainingPath).toBe("/autopost-facebook/paris");
246
+ });
247
+
248
+ it("PseoMatrixEngine: resolveServicePage, generateRobotsTxt, and getTelemetryPayload", () => {
249
+ const engine = new PseoMatrixEngine();
250
+ const data = {
251
+ services: [{ slug: "crm", name: "CRM", category: "Sales" }],
252
+ locations: [{ slug: "paris", name: "Paris", country: "France" }],
253
+ };
254
+
255
+ const servicePage = engine.resolveServicePage("crm", "paris", "https://acme.com", data, { brandName: "Acme" });
256
+ expect(servicePage).toBeDefined();
257
+ expect(servicePage?.h1).toContain("CRM");
258
+
259
+ const robots = engine.generateRobotsTxt("https://acme.com", { allowAiBots: true });
260
+ expect(robots).toContain("User-agent: GPTBot");
261
+ expect(robots).toContain("Sitemap: https://acme.com/sitemap.xml");
262
+
263
+ const telemetry = engine.getTelemetryPayload("https://acme.com", data, { brandName: "Acme" });
264
+ expect(telemetry.totalPages).toBeGreaterThan(0);
265
+ expect(telemetry.brandName).toBe("Acme");
266
+ expect(telemetry.servicesCount).toBe(1);
267
+ });
237
268
  });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 🌐 Dynamic i18n & Locale Auto-Detector
3
+ * Automatically detects and synchronizes website locales without requiring hardcoded lists.
4
+ * Compatible with next-intl, next-i18next, i18next, Paraglide, and Astro i18n.
5
+ */
6
+
7
+ export const SUPPORTED_CANONICAL_LOCALES = [
8
+ "en",
9
+ "fr",
10
+ "es",
11
+ "de",
12
+ "it",
13
+ "pt",
14
+ "nl",
15
+ "ru",
16
+ "sv",
17
+ "pl",
18
+ "ja",
19
+ "zh",
20
+ "ar",
21
+ ] as const;
22
+
23
+ export type SupportedLocale = (typeof SUPPORTED_CANONICAL_LOCALES)[number];
24
+
25
+ export class I18nDetector {
26
+ /**
27
+ * Resolves a clean 2-letter ISO language code from any locale string (e.g. "fr-FR" -> "fr").
28
+ */
29
+ static normalizeLocale(localeInput?: string): string {
30
+ if (!localeInput || typeof localeInput !== "string") return "en";
31
+ const clean = localeInput.toLowerCase().split(/[-_]/)[0].trim();
32
+ return clean || "en";
33
+ }
34
+
35
+ /**
36
+ * Auto-detects and extracts active locales from common environment variables or array inputs.
37
+ */
38
+ static resolveActiveLocales(explicitLocales?: string[] | string): string[] {
39
+ if (explicitLocales) {
40
+ const list = Array.isArray(explicitLocales) ? explicitLocales : [explicitLocales];
41
+ const normalized = list.map((l) => this.normalizeLocale(l)).filter(Boolean);
42
+ return Array.from(new Set(normalized));
43
+ }
44
+
45
+ // Default fallback
46
+ return ["en", "fr"];
47
+ }
48
+
49
+ /**
50
+ * Matches whether a given path segment is a language prefix (e.g. "/fr/autopost-facebook/paris").
51
+ */
52
+ static extractLocaleFromPath(pathname: string, activeLocales: string[]): { locale: string; remainingPath: string } {
53
+ const cleanPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
54
+ const segments = cleanPath.split("/").filter(Boolean);
55
+
56
+ if (segments.length > 0 && activeLocales.includes(segments[0].toLowerCase())) {
57
+ const locale = segments[0].toLowerCase();
58
+ const remainingSegments = segments.slice(1);
59
+ return {
60
+ locale,
61
+ remainingPath: `/${remainingSegments.join("/")}`,
62
+ };
63
+ }
64
+
65
+ return {
66
+ locale: activeLocales[0] || "en",
67
+ remainingPath: cleanPath,
68
+ };
69
+ }
70
+ }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./slug-engine";
15
15
  export * from "./legal-disclaimers";
16
16
  export * from "./matrix-engine";
17
17
  export * from "./built-in-locations";
18
+ export * from "./i18n-detector";
18
19
  export * from "./brand-icons";
19
20
  export * from "./urlytics-engine";
20
21
  export * from "./keyword-permutator";
@@ -883,6 +883,22 @@ ${urls}
883
883
  </urlset>`;
884
884
  }
885
885
 
886
+ /**
887
+ * Resolves a page for a specific module/service and city.
888
+ * Useful when organizing code as app/[service]/[city]/page.tsx
889
+ */
890
+ resolveServicePage(
891
+ serviceSlug: string,
892
+ citySlug: string,
893
+ domain: string,
894
+ data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
895
+ options: MatrixOptions,
896
+ ): GeneratedPageMeta | undefined {
897
+ const cleanService = cleanSeoSlug(serviceSlug, { language: options.language });
898
+ const cleanCity = cleanSeoSlug(citySlug, { language: options.language });
899
+ return this.resolvePage([cleanService, cleanCity], domain, data, options);
900
+ }
901
+
886
902
  /**
887
903
  * Generates an official /llms.txt Markdown directory for AI search bots.
888
904
  */
@@ -905,4 +921,73 @@ ${urls}
905
921
 
906
922
  return lines.join("\n");
907
923
  }
924
+
925
+ /**
926
+ * Generates a modern robots.txt allowing search engines and AI agents with sitemap link.
927
+ */
928
+ generateRobotsTxt(
929
+ domain: string,
930
+ options?: { sitemapUrl?: string; allowAiBots?: boolean; disallowPaths?: string[] },
931
+ ): string {
932
+ const cleanDomain = domain.replace(/\/+$/, "");
933
+ const sitemap = options?.sitemapUrl || `${cleanDomain}/sitemap.xml`;
934
+ const disallows = options?.disallowPaths || ["/api/", "/admin/", "/private/"];
935
+
936
+ const lines: string[] = [
937
+ "User-agent: *",
938
+ "Allow: /",
939
+ ...disallows.map((d) => `Disallow: ${d}`),
940
+ "",
941
+ ];
942
+
943
+ if (options?.allowAiBots !== false) {
944
+ lines.push(
945
+ "# AI Search Crawlers Optimization (AEO/GEO)",
946
+ "User-agent: GPTBot",
947
+ "Allow: /",
948
+ "",
949
+ "User-agent: ClaudeBot",
950
+ "Allow: /",
951
+ "",
952
+ "User-agent: PerplexityBot",
953
+ "Allow: /",
954
+ "",
955
+ "User-agent: Applebot",
956
+ "Allow: /",
957
+ "",
958
+ );
959
+ }
960
+
961
+ lines.push(`Sitemap: ${sitemap}`);
962
+ return lines.join("\n");
963
+ }
964
+
965
+ /**
966
+ * Computes telemetry & page metering data for syncing with LynxSEO Studio dashboard.
967
+ */
968
+ getTelemetryPayload(
969
+ domain: string,
970
+ data: Parameters<PseoMatrixEngine["generateAllMatrices"]>[1],
971
+ options: MatrixOptions,
972
+ ) {
973
+ const pages = this.generateAllMatrices(domain, data, options);
974
+ const indexedPages = pages.filter((p) => p.robots.includes("index"));
975
+
976
+ const matrixBreakdown: Record<string, number> = {};
977
+ for (const p of pages) {
978
+ matrixBreakdown[p.matrixFamily] = (matrixBreakdown[p.matrixFamily] || 0) + 1;
979
+ }
980
+
981
+ return {
982
+ domain,
983
+ brandName: options.brandName,
984
+ language: options.language || "en",
985
+ languages: options.languages || [options.language || "en"],
986
+ totalPages: pages.length,
987
+ indexedPages: indexedPages.length,
988
+ servicesCount: data.services?.length ?? 0,
989
+ matrixBreakdown,
990
+ timestamp: new Date().toISOString(),
991
+ };
992
+ }
908
993
  }