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 +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +128 -5
- package/dist/index.mjs +128 -5
- package/package.json +1 -1
- package/src/client/newsletter-client.ts +52 -0
- package/src/loaders.ts +62 -5
- package/src/types/index.ts +3 -0
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
|
|
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
|
|
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
|
@@ -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
|
|
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;
|
package/src/types/index.ts
CHANGED
|
@@ -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;
|