perspectapi-ts-sdk 2.3.4 → 2.4.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.
package/dist/index.d.mts CHANGED
@@ -276,6 +276,8 @@ interface Content {
276
276
  slug_prefix?: string;
277
277
  pageStatus: ContentStatus;
278
278
  pageType: ContentType;
279
+ media?: MediaItem[] | MediaItem[][];
280
+ image?: string;
279
281
  userId?: number;
280
282
  organizationId?: number;
281
283
  createdAt: string;
@@ -376,6 +378,7 @@ interface BlogPost {
376
378
  content?: string;
377
379
  excerpt?: string;
378
380
  image?: string;
381
+ media?: MediaItem[] | MediaItem[][];
379
382
  published?: boolean;
380
383
  created_at?: string;
381
384
  updated_at?: string;
@@ -1988,6 +1991,32 @@ declare class NewsletterClient extends BaseClient {
1988
1991
  message: string;
1989
1992
  sent: boolean;
1990
1993
  }>>;
1994
+ /**
1995
+ * Track email open event.
1996
+ * Called by client sites when their tracking pixel route is hit.
1997
+ * The client site serves the pixel and calls this to record the open.
1998
+ * @param siteName - The site name
1999
+ * @param token - The tracking token from the pixel URL
2000
+ */
2001
+ trackOpen(siteName: string, token: string): Promise<ApiResponse<{
2002
+ success: boolean;
2003
+ }>>;
2004
+ /**
2005
+ * Track link click event.
2006
+ * Called by client sites when their click tracking route is hit.
2007
+ * The client site redirects to the destination and calls this to record the click.
2008
+ * @param siteName - The site name
2009
+ * @param token - The tracking token from the click URL
2010
+ * @param url - The destination URL that was clicked
2011
+ */
2012
+ trackClick(siteName: string, token: string, url: string): Promise<ApiResponse<{
2013
+ success: boolean;
2014
+ }>>;
2015
+ /**
2016
+ * Get the 1x1 transparent GIF pixel as a Uint8Array.
2017
+ * Client sites can use this to serve the tracking pixel response.
2018
+ */
2019
+ static getTrackingPixel(): Uint8Array;
1991
2020
  }
1992
2021
 
1993
2022
  /**
@@ -2283,7 +2312,7 @@ declare const transformProduct: (perspectProduct: any, logger?: LoaderLogger) =>
2283
2312
  /**
2284
2313
  * Transform PerspectAPI content payload to a simplified BlogPost structure.
2285
2314
  */
2286
- declare const transformContent: (perspectContent: Content | (Content & Record<string, unknown>)) => BlogPost;
2315
+ declare const transformContent: (perspectContent: Content | (Content & Record<string, unknown>), logger?: LoaderLogger) => BlogPost;
2287
2316
  interface LoaderOptions {
2288
2317
  /**
2289
2318
  * Pre-configured PerspectAPI client. Pass null/undefined to force fallback behaviour.
package/dist/index.d.ts CHANGED
@@ -276,6 +276,8 @@ interface Content {
276
276
  slug_prefix?: string;
277
277
  pageStatus: ContentStatus;
278
278
  pageType: ContentType;
279
+ media?: MediaItem[] | MediaItem[][];
280
+ image?: string;
279
281
  userId?: number;
280
282
  organizationId?: number;
281
283
  createdAt: string;
@@ -376,6 +378,7 @@ interface BlogPost {
376
378
  content?: string;
377
379
  excerpt?: string;
378
380
  image?: string;
381
+ media?: MediaItem[] | MediaItem[][];
379
382
  published?: boolean;
380
383
  created_at?: string;
381
384
  updated_at?: string;
@@ -1988,6 +1991,32 @@ declare class NewsletterClient extends BaseClient {
1988
1991
  message: string;
1989
1992
  sent: boolean;
1990
1993
  }>>;
1994
+ /**
1995
+ * Track email open event.
1996
+ * Called by client sites when their tracking pixel route is hit.
1997
+ * The client site serves the pixel and calls this to record the open.
1998
+ * @param siteName - The site name
1999
+ * @param token - The tracking token from the pixel URL
2000
+ */
2001
+ trackOpen(siteName: string, token: string): Promise<ApiResponse<{
2002
+ success: boolean;
2003
+ }>>;
2004
+ /**
2005
+ * Track link click event.
2006
+ * Called by client sites when their click tracking route is hit.
2007
+ * The client site redirects to the destination and calls this to record the click.
2008
+ * @param siteName - The site name
2009
+ * @param token - The tracking token from the click URL
2010
+ * @param url - The destination URL that was clicked
2011
+ */
2012
+ trackClick(siteName: string, token: string, url: string): Promise<ApiResponse<{
2013
+ success: boolean;
2014
+ }>>;
2015
+ /**
2016
+ * Get the 1x1 transparent GIF pixel as a Uint8Array.
2017
+ * Client sites can use this to serve the tracking pixel response.
2018
+ */
2019
+ static getTrackingPixel(): Uint8Array;
1991
2020
  }
1992
2021
 
1993
2022
  /**
@@ -2283,7 +2312,7 @@ declare const transformProduct: (perspectProduct: any, logger?: LoaderLogger) =>
2283
2312
  /**
2284
2313
  * Transform PerspectAPI content payload to a simplified BlogPost structure.
2285
2314
  */
2286
- declare const transformContent: (perspectContent: Content | (Content & Record<string, unknown>)) => BlogPost;
2315
+ declare const transformContent: (perspectContent: Content | (Content & Record<string, unknown>), logger?: LoaderLogger) => BlogPost;
2287
2316
  interface LoaderOptions {
2288
2317
  /**
2289
2318
  * Pre-configured PerspectAPI client. Pass null/undefined to force fallback behaviour.
package/dist/index.js CHANGED
@@ -2076,6 +2076,86 @@ var NewsletterClient = class extends BaseClient {
2076
2076
  data
2077
2077
  );
2078
2078
  }
2079
+ // ============================================
2080
+ // Tracking methods (for first-party tracking)
2081
+ // ============================================
2082
+ /**
2083
+ * Track email open event.
2084
+ * Called by client sites when their tracking pixel route is hit.
2085
+ * The client site serves the pixel and calls this to record the open.
2086
+ * @param siteName - The site name
2087
+ * @param token - The tracking token from the pixel URL
2088
+ */
2089
+ async trackOpen(siteName, token) {
2090
+ return this.http.post(
2091
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/open/${encodeURIComponent(token)}`))
2092
+ );
2093
+ }
2094
+ /**
2095
+ * Track link click event.
2096
+ * Called by client sites when their click tracking route is hit.
2097
+ * The client site redirects to the destination and calls this to record the click.
2098
+ * @param siteName - The site name
2099
+ * @param token - The tracking token from the click URL
2100
+ * @param url - The destination URL that was clicked
2101
+ */
2102
+ async trackClick(siteName, token, url) {
2103
+ return this.http.post(
2104
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/click/${encodeURIComponent(token)}`)),
2105
+ { url }
2106
+ );
2107
+ }
2108
+ /**
2109
+ * Get the 1x1 transparent GIF pixel as a Uint8Array.
2110
+ * Client sites can use this to serve the tracking pixel response.
2111
+ */
2112
+ static getTrackingPixel() {
2113
+ return new Uint8Array([
2114
+ 71,
2115
+ 73,
2116
+ 70,
2117
+ 56,
2118
+ 57,
2119
+ 97,
2120
+ 1,
2121
+ 0,
2122
+ 1,
2123
+ 0,
2124
+ 128,
2125
+ 0,
2126
+ 0,
2127
+ 255,
2128
+ 255,
2129
+ 255,
2130
+ 0,
2131
+ 0,
2132
+ 0,
2133
+ 33,
2134
+ 249,
2135
+ 4,
2136
+ 1,
2137
+ 0,
2138
+ 0,
2139
+ 0,
2140
+ 0,
2141
+ 44,
2142
+ 0,
2143
+ 0,
2144
+ 0,
2145
+ 0,
2146
+ 1,
2147
+ 0,
2148
+ 1,
2149
+ 0,
2150
+ 0,
2151
+ 2,
2152
+ 2,
2153
+ 68,
2154
+ 1,
2155
+ 0,
2156
+ 59
2157
+ ]);
2158
+ }
2079
2159
  };
2080
2160
 
2081
2161
  // src/perspect-api-client.ts
@@ -2323,6 +2403,29 @@ var normalizeMediaList = (rawMedia) => {
2323
2403
  }
2324
2404
  return rawMedia.filter(isMediaItem);
2325
2405
  };
2406
+ var describeMedia = (rawMedia) => {
2407
+ const isArray = Array.isArray(rawMedia);
2408
+ const nestedArray = isArray && rawMedia.length > 0 && Array.isArray(rawMedia[0]);
2409
+ const rawCount = isArray ? rawMedia.length : 0;
2410
+ const flattenedCount = nestedArray ? rawMedia.flat().length : rawCount;
2411
+ const firstSample = (() => {
2412
+ if (!isArray) {
2413
+ return rawMedia;
2414
+ }
2415
+ if (nestedArray) {
2416
+ const firstGroup = rawMedia.find(Array.isArray);
2417
+ return firstGroup ? firstGroup[0] : void 0;
2418
+ }
2419
+ return rawMedia[0];
2420
+ })();
2421
+ return {
2422
+ isArray,
2423
+ nestedArray,
2424
+ rawCount,
2425
+ flattenedCount,
2426
+ sampleType: firstSample ? typeof firstSample : "undefined"
2427
+ };
2428
+ };
2326
2429
  var normalizeQueryParamList = (value) => {
2327
2430
  if (value === void 0 || value === null) {
2328
2431
  return void 0;
@@ -2365,9 +2468,28 @@ var transformProduct = (perspectProduct, logger) => {
2365
2468
  ...rawProduct
2366
2469
  };
2367
2470
  };
2368
- var transformContent = (perspectContent) => {
2471
+ var transformContent = (perspectContent, logger) => {
2369
2472
  const raw = perspectContent ?? {};
2473
+ const mediaItems = normalizeMediaList(raw.media);
2474
+ const image = raw.image;
2475
+ if (raw.media !== void 0) {
2476
+ const mediaDetails = describeMedia(raw.media);
2477
+ const slugOrId = raw.slug ?? raw.id;
2478
+ if (mediaItems.length === 0 && (mediaDetails.rawCount > 0 || mediaDetails.flattenedCount > 0)) {
2479
+ log(logger, "debug", "[PerspectAPI] Content media present but failed normalization", {
2480
+ slug: slugOrId,
2481
+ ...mediaDetails
2482
+ });
2483
+ } else {
2484
+ log(logger, "debug", "[PerspectAPI] Content media normalization", {
2485
+ slug: slugOrId,
2486
+ ...mediaDetails,
2487
+ normalizedCount: mediaItems.length
2488
+ });
2489
+ }
2490
+ }
2370
2491
  return {
2492
+ ...raw,
2371
2493
  id: typeof raw.id === "string" ? raw.id : String(raw.id ?? ""),
2372
2494
  slug: raw.slug ?? `post-${raw.id ?? "unknown"}`,
2373
2495
  slug_prefix: raw.slug_prefix ?? (raw.pageType === "post" ? "blog" : "page"),
@@ -2382,7 +2504,8 @@ var transformContent = (perspectContent) => {
2382
2504
  published_date: raw.published_date,
2383
2505
  author: raw.author,
2384
2506
  tags: raw.tags,
2385
- image: raw.image
2507
+ image,
2508
+ media: mediaItems
2386
2509
  };
2387
2510
  };
2388
2511
  var getDefaultFallbackProducts = (siteName) => {
@@ -2568,7 +2691,7 @@ async function loadPages(options) {
2568
2691
  if (!response.data) {
2569
2692
  return [];
2570
2693
  }
2571
- return response.data.map((content) => transformContent(content));
2694
+ return response.data.map((content) => transformContent(content, logger));
2572
2695
  } catch (error) {
2573
2696
  log(logger, "error", "[PerspectAPI] Error loading pages", error);
2574
2697
  return [];
@@ -2602,7 +2725,7 @@ async function loadPosts(options) {
2602
2725
  log(logger, "warn", "[PerspectAPI] Posts response missing data");
2603
2726
  return [];
2604
2727
  }
2605
- return response.data.map((content) => transformContent(content));
2728
+ return response.data.map((content) => transformContent(content, logger));
2606
2729
  } catch (error) {
2607
2730
  log(logger, "error", "[PerspectAPI] Error loading posts", error);
2608
2731
  return [];
@@ -2628,7 +2751,7 @@ async function loadContentBySlug(options) {
2628
2751
  log(logger, "warn", `[PerspectAPI] Content not found for slug "${slug}"`);
2629
2752
  return null;
2630
2753
  }
2631
- return transformContent(response.data);
2754
+ return transformContent(response.data, logger);
2632
2755
  } catch (error) {
2633
2756
  log(logger, "error", `[PerspectAPI] Error loading content slug "${slug}"`, error);
2634
2757
  return null;
package/dist/index.mjs CHANGED
@@ -2015,6 +2015,86 @@ var NewsletterClient = class extends BaseClient {
2015
2015
  data
2016
2016
  );
2017
2017
  }
2018
+ // ============================================
2019
+ // Tracking methods (for first-party tracking)
2020
+ // ============================================
2021
+ /**
2022
+ * Track email open event.
2023
+ * Called by client sites when their tracking pixel route is hit.
2024
+ * The client site serves the pixel and calls this to record the open.
2025
+ * @param siteName - The site name
2026
+ * @param token - The tracking token from the pixel URL
2027
+ */
2028
+ async trackOpen(siteName, token) {
2029
+ return this.http.post(
2030
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/open/${encodeURIComponent(token)}`))
2031
+ );
2032
+ }
2033
+ /**
2034
+ * Track link click event.
2035
+ * Called by client sites when their click tracking route is hit.
2036
+ * The client site redirects to the destination and calls this to record the click.
2037
+ * @param siteName - The site name
2038
+ * @param token - The tracking token from the click URL
2039
+ * @param url - The destination URL that was clicked
2040
+ */
2041
+ async trackClick(siteName, token, url) {
2042
+ return this.http.post(
2043
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/click/${encodeURIComponent(token)}`)),
2044
+ { url }
2045
+ );
2046
+ }
2047
+ /**
2048
+ * Get the 1x1 transparent GIF pixel as a Uint8Array.
2049
+ * Client sites can use this to serve the tracking pixel response.
2050
+ */
2051
+ static getTrackingPixel() {
2052
+ return new Uint8Array([
2053
+ 71,
2054
+ 73,
2055
+ 70,
2056
+ 56,
2057
+ 57,
2058
+ 97,
2059
+ 1,
2060
+ 0,
2061
+ 1,
2062
+ 0,
2063
+ 128,
2064
+ 0,
2065
+ 0,
2066
+ 255,
2067
+ 255,
2068
+ 255,
2069
+ 0,
2070
+ 0,
2071
+ 0,
2072
+ 33,
2073
+ 249,
2074
+ 4,
2075
+ 1,
2076
+ 0,
2077
+ 0,
2078
+ 0,
2079
+ 0,
2080
+ 44,
2081
+ 0,
2082
+ 0,
2083
+ 0,
2084
+ 0,
2085
+ 1,
2086
+ 0,
2087
+ 1,
2088
+ 0,
2089
+ 0,
2090
+ 2,
2091
+ 2,
2092
+ 68,
2093
+ 1,
2094
+ 0,
2095
+ 59
2096
+ ]);
2097
+ }
2018
2098
  };
2019
2099
 
2020
2100
  // src/perspect-api-client.ts
@@ -2262,6 +2342,29 @@ var normalizeMediaList = (rawMedia) => {
2262
2342
  }
2263
2343
  return rawMedia.filter(isMediaItem);
2264
2344
  };
2345
+ var describeMedia = (rawMedia) => {
2346
+ const isArray = Array.isArray(rawMedia);
2347
+ const nestedArray = isArray && rawMedia.length > 0 && Array.isArray(rawMedia[0]);
2348
+ const rawCount = isArray ? rawMedia.length : 0;
2349
+ const flattenedCount = nestedArray ? rawMedia.flat().length : rawCount;
2350
+ const firstSample = (() => {
2351
+ if (!isArray) {
2352
+ return rawMedia;
2353
+ }
2354
+ if (nestedArray) {
2355
+ const firstGroup = rawMedia.find(Array.isArray);
2356
+ return firstGroup ? firstGroup[0] : void 0;
2357
+ }
2358
+ return rawMedia[0];
2359
+ })();
2360
+ return {
2361
+ isArray,
2362
+ nestedArray,
2363
+ rawCount,
2364
+ flattenedCount,
2365
+ sampleType: firstSample ? typeof firstSample : "undefined"
2366
+ };
2367
+ };
2265
2368
  var normalizeQueryParamList = (value) => {
2266
2369
  if (value === void 0 || value === null) {
2267
2370
  return void 0;
@@ -2304,9 +2407,28 @@ var transformProduct = (perspectProduct, logger) => {
2304
2407
  ...rawProduct
2305
2408
  };
2306
2409
  };
2307
- var transformContent = (perspectContent) => {
2410
+ var transformContent = (perspectContent, logger) => {
2308
2411
  const raw = perspectContent ?? {};
2412
+ const mediaItems = normalizeMediaList(raw.media);
2413
+ const image = raw.image;
2414
+ if (raw.media !== void 0) {
2415
+ const mediaDetails = describeMedia(raw.media);
2416
+ const slugOrId = raw.slug ?? raw.id;
2417
+ if (mediaItems.length === 0 && (mediaDetails.rawCount > 0 || mediaDetails.flattenedCount > 0)) {
2418
+ log(logger, "debug", "[PerspectAPI] Content media present but failed normalization", {
2419
+ slug: slugOrId,
2420
+ ...mediaDetails
2421
+ });
2422
+ } else {
2423
+ log(logger, "debug", "[PerspectAPI] Content media normalization", {
2424
+ slug: slugOrId,
2425
+ ...mediaDetails,
2426
+ normalizedCount: mediaItems.length
2427
+ });
2428
+ }
2429
+ }
2309
2430
  return {
2431
+ ...raw,
2310
2432
  id: typeof raw.id === "string" ? raw.id : String(raw.id ?? ""),
2311
2433
  slug: raw.slug ?? `post-${raw.id ?? "unknown"}`,
2312
2434
  slug_prefix: raw.slug_prefix ?? (raw.pageType === "post" ? "blog" : "page"),
@@ -2321,7 +2443,8 @@ var transformContent = (perspectContent) => {
2321
2443
  published_date: raw.published_date,
2322
2444
  author: raw.author,
2323
2445
  tags: raw.tags,
2324
- image: raw.image
2446
+ image,
2447
+ media: mediaItems
2325
2448
  };
2326
2449
  };
2327
2450
  var getDefaultFallbackProducts = (siteName) => {
@@ -2507,7 +2630,7 @@ async function loadPages(options) {
2507
2630
  if (!response.data) {
2508
2631
  return [];
2509
2632
  }
2510
- return response.data.map((content) => transformContent(content));
2633
+ return response.data.map((content) => transformContent(content, logger));
2511
2634
  } catch (error) {
2512
2635
  log(logger, "error", "[PerspectAPI] Error loading pages", error);
2513
2636
  return [];
@@ -2541,7 +2664,7 @@ async function loadPosts(options) {
2541
2664
  log(logger, "warn", "[PerspectAPI] Posts response missing data");
2542
2665
  return [];
2543
2666
  }
2544
- return response.data.map((content) => transformContent(content));
2667
+ return response.data.map((content) => transformContent(content, logger));
2545
2668
  } catch (error) {
2546
2669
  log(logger, "error", "[PerspectAPI] Error loading posts", error);
2547
2670
  return [];
@@ -2567,7 +2690,7 @@ async function loadContentBySlug(options) {
2567
2690
  log(logger, "warn", `[PerspectAPI] Content not found for slug "${slug}"`);
2568
2691
  return null;
2569
2692
  }
2570
- return transformContent(response.data);
2693
+ return transformContent(response.data, logger);
2571
2694
  } catch (error) {
2572
2695
  log(logger, "error", `[PerspectAPI] Error loading content slug "${slug}"`, error);
2573
2696
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -380,4 +380,56 @@ export class NewsletterClient extends BaseClient {
380
380
  data
381
381
  );
382
382
  }
383
+
384
+ // ============================================
385
+ // Tracking methods (for first-party tracking)
386
+ // ============================================
387
+
388
+ /**
389
+ * Track email open event.
390
+ * Called by client sites when their tracking pixel route is hit.
391
+ * The client site serves the pixel and calls this to record the open.
392
+ * @param siteName - The site name
393
+ * @param token - The tracking token from the pixel URL
394
+ */
395
+ async trackOpen(
396
+ siteName: string,
397
+ token: string
398
+ ): Promise<ApiResponse<{ success: boolean }>> {
399
+ return this.http.post(
400
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/open/${encodeURIComponent(token)}`))
401
+ );
402
+ }
403
+
404
+ /**
405
+ * Track link click event.
406
+ * Called by client sites when their click tracking route is hit.
407
+ * The client site redirects to the destination and calls this to record the click.
408
+ * @param siteName - The site name
409
+ * @param token - The tracking token from the click URL
410
+ * @param url - The destination URL that was clicked
411
+ */
412
+ async trackClick(
413
+ siteName: string,
414
+ token: string,
415
+ url: string
416
+ ): Promise<ApiResponse<{ success: boolean }>> {
417
+ return this.http.post(
418
+ this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/click/${encodeURIComponent(token)}`)),
419
+ { url }
420
+ );
421
+ }
422
+
423
+ /**
424
+ * Get the 1x1 transparent GIF pixel as a Uint8Array.
425
+ * Client sites can use this to serve the tracking pixel response.
426
+ */
427
+ static getTrackingPixel(): Uint8Array {
428
+ return new Uint8Array([
429
+ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00,
430
+ 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00,
431
+ 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02,
432
+ 0x44, 0x01, 0x00, 0x3b,
433
+ ]);
434
+ }
383
435
  }
package/src/loaders.ts CHANGED
@@ -72,6 +72,40 @@ const normalizeMediaList = (rawMedia: unknown): MediaItem[] => {
72
72
  return (rawMedia as unknown[]).filter(isMediaItem);
73
73
  };
74
74
 
75
+ const describeMedia = (rawMedia: unknown) => {
76
+ const isArray = Array.isArray(rawMedia);
77
+ const nestedArray =
78
+ isArray &&
79
+ (rawMedia as unknown[]).length > 0 &&
80
+ Array.isArray((rawMedia as unknown[])[0]);
81
+
82
+ const rawCount = isArray ? (rawMedia as unknown[]).length : 0;
83
+ const flattenedCount = nestedArray
84
+ ? (rawMedia as unknown[]).flat().length
85
+ : rawCount;
86
+
87
+ const firstSample = (() => {
88
+ if (!isArray) {
89
+ return rawMedia;
90
+ }
91
+ if (nestedArray) {
92
+ const firstGroup = (rawMedia as unknown[]).find(Array.isArray) as
93
+ | unknown[]
94
+ | undefined;
95
+ return firstGroup ? firstGroup[0] : undefined;
96
+ }
97
+ return (rawMedia as unknown[])[0];
98
+ })();
99
+
100
+ return {
101
+ isArray,
102
+ nestedArray,
103
+ rawCount,
104
+ flattenedCount,
105
+ sampleType: firstSample ? typeof firstSample : 'undefined'
106
+ };
107
+ };
108
+
75
109
  const normalizeQueryParamList = (
76
110
  value: string | number | Array<string | number> | undefined
77
111
  ): string | undefined => {
@@ -148,11 +182,33 @@ export const transformProduct = (
148
182
  * Transform PerspectAPI content payload to a simplified BlogPost structure.
149
183
  */
150
184
  export const transformContent = (
151
- perspectContent: Content | (Content & Record<string, unknown>)
185
+ perspectContent: Content | (Content & Record<string, unknown>),
186
+ logger?: LoaderLogger
152
187
  ): BlogPost => {
153
188
  const raw = perspectContent ?? ({} as Content);
189
+ const mediaItems = normalizeMediaList((raw as any).media);
190
+ const image = (raw as any).image;
191
+
192
+ if ((raw as any).media !== undefined) {
193
+ const mediaDetails = describeMedia((raw as any).media);
194
+ const slugOrId = raw.slug ?? raw.id;
195
+
196
+ if (mediaItems.length === 0 && (mediaDetails.rawCount > 0 || mediaDetails.flattenedCount > 0)) {
197
+ log(logger, 'debug', '[PerspectAPI] Content media present but failed normalization', {
198
+ slug: slugOrId,
199
+ ...mediaDetails
200
+ });
201
+ } else {
202
+ log(logger, 'debug', '[PerspectAPI] Content media normalization', {
203
+ slug: slugOrId,
204
+ ...mediaDetails,
205
+ normalizedCount: mediaItems.length
206
+ });
207
+ }
208
+ }
154
209
 
155
210
  return {
211
+ ...raw,
156
212
  id: typeof raw.id === 'string' ? raw.id : String(raw.id ?? ''),
157
213
  slug: raw.slug ?? `post-${raw.id ?? 'unknown'}`,
158
214
  slug_prefix:
@@ -172,7 +228,8 @@ export const transformContent = (
172
228
  published_date: (raw as any).published_date,
173
229
  author: (raw as any).author,
174
230
  tags: (raw as any).tags,
175
- image: (raw as any).image
231
+ image,
232
+ media: mediaItems
176
233
  };
177
234
  };
178
235
 
@@ -463,7 +520,7 @@ export async function loadPages(
463
520
  return [];
464
521
  }
465
522
 
466
- return response.data.map(content => transformContent(content));
523
+ return response.data.map(content => transformContent(content, logger));
467
524
  } catch (error) {
468
525
  log(logger, 'error', '[PerspectAPI] Error loading pages', error);
469
526
  return [];
@@ -510,7 +567,7 @@ export async function loadPosts(
510
567
  return [];
511
568
  }
512
569
 
513
- return response.data.map(content => transformContent(content));
570
+ return response.data.map(content => transformContent(content, logger));
514
571
  } catch (error) {
515
572
  log(logger, 'error', '[PerspectAPI] Error loading posts', error);
516
573
  return [];
@@ -551,7 +608,7 @@ export async function loadContentBySlug(
551
608
  return null;
552
609
  }
553
610
 
554
- return transformContent(response.data);
611
+ return transformContent(response.data, logger);
555
612
  } catch (error) {
556
613
  log(logger, 'error', `[PerspectAPI] Error loading content slug "${slug}"`, error);
557
614
  return null;
@@ -226,6 +226,8 @@ export interface Content {
226
226
  slug_prefix?: string;
227
227
  pageStatus: ContentStatus;
228
228
  pageType: ContentType;
229
+ media?: MediaItem[] | MediaItem[][];
230
+ image?: string;
229
231
  userId?: number;
230
232
  organizationId?: number;
231
233
  createdAt: string;
@@ -338,6 +340,7 @@ export interface BlogPost {
338
340
  content?: string;
339
341
  excerpt?: string;
340
342
  image?: string;
343
+ media?: MediaItem[] | MediaItem[][];
341
344
  published?: boolean;
342
345
  created_at?: string;
343
346
  updated_at?: string;