perspectapi-ts-sdk 2.3.4 → 2.5.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 +134 -8
- package/dist/index.mjs +134 -8
- package/package.json +1 -1
- package/src/client/content-client.ts +6 -3
- 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
|
@@ -850,12 +850,12 @@ var ContentClient = class extends BaseClient {
|
|
|
850
850
|
console.log("[PerspectAPI Content] Cache details:", {
|
|
851
851
|
cacheEnabled: !!this.cache,
|
|
852
852
|
cachePolicy,
|
|
853
|
-
tags: this.buildContentTags(siteName)
|
|
853
|
+
tags: this.buildContentTags(siteName, void 0, void 0, normalizedParams?.slug_prefix)
|
|
854
854
|
});
|
|
855
855
|
const result = await this.fetchWithCache(
|
|
856
856
|
endpoint,
|
|
857
857
|
normalizedParams,
|
|
858
|
-
this.buildContentTags(siteName),
|
|
858
|
+
this.buildContentTags(siteName, void 0, void 0, normalizedParams?.slug_prefix),
|
|
859
859
|
cachePolicy,
|
|
860
860
|
async () => {
|
|
861
861
|
console.log("[PerspectAPI Content] Making HTTP request:", {
|
|
@@ -976,7 +976,7 @@ var ContentClient = class extends BaseClient {
|
|
|
976
976
|
async duplicateContent(id) {
|
|
977
977
|
return this.create(`/content/${id}/duplicate`, {});
|
|
978
978
|
}
|
|
979
|
-
buildContentTags(siteName, slug, id) {
|
|
979
|
+
buildContentTags(siteName, slug, id, slugPrefix) {
|
|
980
980
|
const tags = /* @__PURE__ */ new Set(["content"]);
|
|
981
981
|
if (siteName) {
|
|
982
982
|
tags.add(`content:site:${siteName}`);
|
|
@@ -987,6 +987,9 @@ var ContentClient = class extends BaseClient {
|
|
|
987
987
|
if (typeof id === "number") {
|
|
988
988
|
tags.add(`content:id:${id}`);
|
|
989
989
|
}
|
|
990
|
+
if (slugPrefix) {
|
|
991
|
+
tags.add(`content:prefix:${slugPrefix}`);
|
|
992
|
+
}
|
|
990
993
|
return Array.from(tags.values());
|
|
991
994
|
}
|
|
992
995
|
};
|
|
@@ -2076,6 +2079,86 @@ var NewsletterClient = class extends BaseClient {
|
|
|
2076
2079
|
data
|
|
2077
2080
|
);
|
|
2078
2081
|
}
|
|
2082
|
+
// ============================================
|
|
2083
|
+
// Tracking methods (for first-party tracking)
|
|
2084
|
+
// ============================================
|
|
2085
|
+
/**
|
|
2086
|
+
* Track email open event.
|
|
2087
|
+
* Called by client sites when their tracking pixel route is hit.
|
|
2088
|
+
* The client site serves the pixel and calls this to record the open.
|
|
2089
|
+
* @param siteName - The site name
|
|
2090
|
+
* @param token - The tracking token from the pixel URL
|
|
2091
|
+
*/
|
|
2092
|
+
async trackOpen(siteName, token) {
|
|
2093
|
+
return this.http.post(
|
|
2094
|
+
this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/open/${encodeURIComponent(token)}`))
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Track link click event.
|
|
2099
|
+
* Called by client sites when their click tracking route is hit.
|
|
2100
|
+
* The client site redirects to the destination and calls this to record the click.
|
|
2101
|
+
* @param siteName - The site name
|
|
2102
|
+
* @param token - The tracking token from the click URL
|
|
2103
|
+
* @param url - The destination URL that was clicked
|
|
2104
|
+
*/
|
|
2105
|
+
async trackClick(siteName, token, url) {
|
|
2106
|
+
return this.http.post(
|
|
2107
|
+
this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/click/${encodeURIComponent(token)}`)),
|
|
2108
|
+
{ url }
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Get the 1x1 transparent GIF pixel as a Uint8Array.
|
|
2113
|
+
* Client sites can use this to serve the tracking pixel response.
|
|
2114
|
+
*/
|
|
2115
|
+
static getTrackingPixel() {
|
|
2116
|
+
return new Uint8Array([
|
|
2117
|
+
71,
|
|
2118
|
+
73,
|
|
2119
|
+
70,
|
|
2120
|
+
56,
|
|
2121
|
+
57,
|
|
2122
|
+
97,
|
|
2123
|
+
1,
|
|
2124
|
+
0,
|
|
2125
|
+
1,
|
|
2126
|
+
0,
|
|
2127
|
+
128,
|
|
2128
|
+
0,
|
|
2129
|
+
0,
|
|
2130
|
+
255,
|
|
2131
|
+
255,
|
|
2132
|
+
255,
|
|
2133
|
+
0,
|
|
2134
|
+
0,
|
|
2135
|
+
0,
|
|
2136
|
+
33,
|
|
2137
|
+
249,
|
|
2138
|
+
4,
|
|
2139
|
+
1,
|
|
2140
|
+
0,
|
|
2141
|
+
0,
|
|
2142
|
+
0,
|
|
2143
|
+
0,
|
|
2144
|
+
44,
|
|
2145
|
+
0,
|
|
2146
|
+
0,
|
|
2147
|
+
0,
|
|
2148
|
+
0,
|
|
2149
|
+
1,
|
|
2150
|
+
0,
|
|
2151
|
+
1,
|
|
2152
|
+
0,
|
|
2153
|
+
0,
|
|
2154
|
+
2,
|
|
2155
|
+
2,
|
|
2156
|
+
68,
|
|
2157
|
+
1,
|
|
2158
|
+
0,
|
|
2159
|
+
59
|
|
2160
|
+
]);
|
|
2161
|
+
}
|
|
2079
2162
|
};
|
|
2080
2163
|
|
|
2081
2164
|
// src/perspect-api-client.ts
|
|
@@ -2323,6 +2406,29 @@ var normalizeMediaList = (rawMedia) => {
|
|
|
2323
2406
|
}
|
|
2324
2407
|
return rawMedia.filter(isMediaItem);
|
|
2325
2408
|
};
|
|
2409
|
+
var describeMedia = (rawMedia) => {
|
|
2410
|
+
const isArray = Array.isArray(rawMedia);
|
|
2411
|
+
const nestedArray = isArray && rawMedia.length > 0 && Array.isArray(rawMedia[0]);
|
|
2412
|
+
const rawCount = isArray ? rawMedia.length : 0;
|
|
2413
|
+
const flattenedCount = nestedArray ? rawMedia.flat().length : rawCount;
|
|
2414
|
+
const firstSample = (() => {
|
|
2415
|
+
if (!isArray) {
|
|
2416
|
+
return rawMedia;
|
|
2417
|
+
}
|
|
2418
|
+
if (nestedArray) {
|
|
2419
|
+
const firstGroup = rawMedia.find(Array.isArray);
|
|
2420
|
+
return firstGroup ? firstGroup[0] : void 0;
|
|
2421
|
+
}
|
|
2422
|
+
return rawMedia[0];
|
|
2423
|
+
})();
|
|
2424
|
+
return {
|
|
2425
|
+
isArray,
|
|
2426
|
+
nestedArray,
|
|
2427
|
+
rawCount,
|
|
2428
|
+
flattenedCount,
|
|
2429
|
+
sampleType: firstSample ? typeof firstSample : "undefined"
|
|
2430
|
+
};
|
|
2431
|
+
};
|
|
2326
2432
|
var normalizeQueryParamList = (value) => {
|
|
2327
2433
|
if (value === void 0 || value === null) {
|
|
2328
2434
|
return void 0;
|
|
@@ -2365,9 +2471,28 @@ var transformProduct = (perspectProduct, logger) => {
|
|
|
2365
2471
|
...rawProduct
|
|
2366
2472
|
};
|
|
2367
2473
|
};
|
|
2368
|
-
var transformContent = (perspectContent) => {
|
|
2474
|
+
var transformContent = (perspectContent, logger) => {
|
|
2369
2475
|
const raw = perspectContent ?? {};
|
|
2476
|
+
const mediaItems = normalizeMediaList(raw.media);
|
|
2477
|
+
const image = raw.image;
|
|
2478
|
+
if (raw.media !== void 0) {
|
|
2479
|
+
const mediaDetails = describeMedia(raw.media);
|
|
2480
|
+
const slugOrId = raw.slug ?? raw.id;
|
|
2481
|
+
if (mediaItems.length === 0 && (mediaDetails.rawCount > 0 || mediaDetails.flattenedCount > 0)) {
|
|
2482
|
+
log(logger, "debug", "[PerspectAPI] Content media present but failed normalization", {
|
|
2483
|
+
slug: slugOrId,
|
|
2484
|
+
...mediaDetails
|
|
2485
|
+
});
|
|
2486
|
+
} else {
|
|
2487
|
+
log(logger, "debug", "[PerspectAPI] Content media normalization", {
|
|
2488
|
+
slug: slugOrId,
|
|
2489
|
+
...mediaDetails,
|
|
2490
|
+
normalizedCount: mediaItems.length
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2370
2494
|
return {
|
|
2495
|
+
...raw,
|
|
2371
2496
|
id: typeof raw.id === "string" ? raw.id : String(raw.id ?? ""),
|
|
2372
2497
|
slug: raw.slug ?? `post-${raw.id ?? "unknown"}`,
|
|
2373
2498
|
slug_prefix: raw.slug_prefix ?? (raw.pageType === "post" ? "blog" : "page"),
|
|
@@ -2382,7 +2507,8 @@ var transformContent = (perspectContent) => {
|
|
|
2382
2507
|
published_date: raw.published_date,
|
|
2383
2508
|
author: raw.author,
|
|
2384
2509
|
tags: raw.tags,
|
|
2385
|
-
image
|
|
2510
|
+
image,
|
|
2511
|
+
media: mediaItems
|
|
2386
2512
|
};
|
|
2387
2513
|
};
|
|
2388
2514
|
var getDefaultFallbackProducts = (siteName) => {
|
|
@@ -2568,7 +2694,7 @@ async function loadPages(options) {
|
|
|
2568
2694
|
if (!response.data) {
|
|
2569
2695
|
return [];
|
|
2570
2696
|
}
|
|
2571
|
-
return response.data.map((content) => transformContent(content));
|
|
2697
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
2572
2698
|
} catch (error) {
|
|
2573
2699
|
log(logger, "error", "[PerspectAPI] Error loading pages", error);
|
|
2574
2700
|
return [];
|
|
@@ -2602,7 +2728,7 @@ async function loadPosts(options) {
|
|
|
2602
2728
|
log(logger, "warn", "[PerspectAPI] Posts response missing data");
|
|
2603
2729
|
return [];
|
|
2604
2730
|
}
|
|
2605
|
-
return response.data.map((content) => transformContent(content));
|
|
2731
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
2606
2732
|
} catch (error) {
|
|
2607
2733
|
log(logger, "error", "[PerspectAPI] Error loading posts", error);
|
|
2608
2734
|
return [];
|
|
@@ -2628,7 +2754,7 @@ async function loadContentBySlug(options) {
|
|
|
2628
2754
|
log(logger, "warn", `[PerspectAPI] Content not found for slug "${slug}"`);
|
|
2629
2755
|
return null;
|
|
2630
2756
|
}
|
|
2631
|
-
return transformContent(response.data);
|
|
2757
|
+
return transformContent(response.data, logger);
|
|
2632
2758
|
} catch (error) {
|
|
2633
2759
|
log(logger, "error", `[PerspectAPI] Error loading content slug "${slug}"`, error);
|
|
2634
2760
|
return null;
|
package/dist/index.mjs
CHANGED
|
@@ -789,12 +789,12 @@ var ContentClient = class extends BaseClient {
|
|
|
789
789
|
console.log("[PerspectAPI Content] Cache details:", {
|
|
790
790
|
cacheEnabled: !!this.cache,
|
|
791
791
|
cachePolicy,
|
|
792
|
-
tags: this.buildContentTags(siteName)
|
|
792
|
+
tags: this.buildContentTags(siteName, void 0, void 0, normalizedParams?.slug_prefix)
|
|
793
793
|
});
|
|
794
794
|
const result = await this.fetchWithCache(
|
|
795
795
|
endpoint,
|
|
796
796
|
normalizedParams,
|
|
797
|
-
this.buildContentTags(siteName),
|
|
797
|
+
this.buildContentTags(siteName, void 0, void 0, normalizedParams?.slug_prefix),
|
|
798
798
|
cachePolicy,
|
|
799
799
|
async () => {
|
|
800
800
|
console.log("[PerspectAPI Content] Making HTTP request:", {
|
|
@@ -915,7 +915,7 @@ var ContentClient = class extends BaseClient {
|
|
|
915
915
|
async duplicateContent(id) {
|
|
916
916
|
return this.create(`/content/${id}/duplicate`, {});
|
|
917
917
|
}
|
|
918
|
-
buildContentTags(siteName, slug, id) {
|
|
918
|
+
buildContentTags(siteName, slug, id, slugPrefix) {
|
|
919
919
|
const tags = /* @__PURE__ */ new Set(["content"]);
|
|
920
920
|
if (siteName) {
|
|
921
921
|
tags.add(`content:site:${siteName}`);
|
|
@@ -926,6 +926,9 @@ var ContentClient = class extends BaseClient {
|
|
|
926
926
|
if (typeof id === "number") {
|
|
927
927
|
tags.add(`content:id:${id}`);
|
|
928
928
|
}
|
|
929
|
+
if (slugPrefix) {
|
|
930
|
+
tags.add(`content:prefix:${slugPrefix}`);
|
|
931
|
+
}
|
|
929
932
|
return Array.from(tags.values());
|
|
930
933
|
}
|
|
931
934
|
};
|
|
@@ -2015,6 +2018,86 @@ var NewsletterClient = class extends BaseClient {
|
|
|
2015
2018
|
data
|
|
2016
2019
|
);
|
|
2017
2020
|
}
|
|
2021
|
+
// ============================================
|
|
2022
|
+
// Tracking methods (for first-party tracking)
|
|
2023
|
+
// ============================================
|
|
2024
|
+
/**
|
|
2025
|
+
* Track email open event.
|
|
2026
|
+
* Called by client sites when their tracking pixel route is hit.
|
|
2027
|
+
* The client site serves the pixel and calls this to record the open.
|
|
2028
|
+
* @param siteName - The site name
|
|
2029
|
+
* @param token - The tracking token from the pixel URL
|
|
2030
|
+
*/
|
|
2031
|
+
async trackOpen(siteName, token) {
|
|
2032
|
+
return this.http.post(
|
|
2033
|
+
this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/open/${encodeURIComponent(token)}`))
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Track link click event.
|
|
2038
|
+
* Called by client sites when their click tracking route is hit.
|
|
2039
|
+
* The client site redirects to the destination and calls this to record the click.
|
|
2040
|
+
* @param siteName - The site name
|
|
2041
|
+
* @param token - The tracking token from the click URL
|
|
2042
|
+
* @param url - The destination URL that was clicked
|
|
2043
|
+
*/
|
|
2044
|
+
async trackClick(siteName, token, url) {
|
|
2045
|
+
return this.http.post(
|
|
2046
|
+
this.buildPath(this.newsletterEndpoint(siteName, `/newsletter/track/click/${encodeURIComponent(token)}`)),
|
|
2047
|
+
{ url }
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Get the 1x1 transparent GIF pixel as a Uint8Array.
|
|
2052
|
+
* Client sites can use this to serve the tracking pixel response.
|
|
2053
|
+
*/
|
|
2054
|
+
static getTrackingPixel() {
|
|
2055
|
+
return new Uint8Array([
|
|
2056
|
+
71,
|
|
2057
|
+
73,
|
|
2058
|
+
70,
|
|
2059
|
+
56,
|
|
2060
|
+
57,
|
|
2061
|
+
97,
|
|
2062
|
+
1,
|
|
2063
|
+
0,
|
|
2064
|
+
1,
|
|
2065
|
+
0,
|
|
2066
|
+
128,
|
|
2067
|
+
0,
|
|
2068
|
+
0,
|
|
2069
|
+
255,
|
|
2070
|
+
255,
|
|
2071
|
+
255,
|
|
2072
|
+
0,
|
|
2073
|
+
0,
|
|
2074
|
+
0,
|
|
2075
|
+
33,
|
|
2076
|
+
249,
|
|
2077
|
+
4,
|
|
2078
|
+
1,
|
|
2079
|
+
0,
|
|
2080
|
+
0,
|
|
2081
|
+
0,
|
|
2082
|
+
0,
|
|
2083
|
+
44,
|
|
2084
|
+
0,
|
|
2085
|
+
0,
|
|
2086
|
+
0,
|
|
2087
|
+
0,
|
|
2088
|
+
1,
|
|
2089
|
+
0,
|
|
2090
|
+
1,
|
|
2091
|
+
0,
|
|
2092
|
+
0,
|
|
2093
|
+
2,
|
|
2094
|
+
2,
|
|
2095
|
+
68,
|
|
2096
|
+
1,
|
|
2097
|
+
0,
|
|
2098
|
+
59
|
|
2099
|
+
]);
|
|
2100
|
+
}
|
|
2018
2101
|
};
|
|
2019
2102
|
|
|
2020
2103
|
// src/perspect-api-client.ts
|
|
@@ -2262,6 +2345,29 @@ var normalizeMediaList = (rawMedia) => {
|
|
|
2262
2345
|
}
|
|
2263
2346
|
return rawMedia.filter(isMediaItem);
|
|
2264
2347
|
};
|
|
2348
|
+
var describeMedia = (rawMedia) => {
|
|
2349
|
+
const isArray = Array.isArray(rawMedia);
|
|
2350
|
+
const nestedArray = isArray && rawMedia.length > 0 && Array.isArray(rawMedia[0]);
|
|
2351
|
+
const rawCount = isArray ? rawMedia.length : 0;
|
|
2352
|
+
const flattenedCount = nestedArray ? rawMedia.flat().length : rawCount;
|
|
2353
|
+
const firstSample = (() => {
|
|
2354
|
+
if (!isArray) {
|
|
2355
|
+
return rawMedia;
|
|
2356
|
+
}
|
|
2357
|
+
if (nestedArray) {
|
|
2358
|
+
const firstGroup = rawMedia.find(Array.isArray);
|
|
2359
|
+
return firstGroup ? firstGroup[0] : void 0;
|
|
2360
|
+
}
|
|
2361
|
+
return rawMedia[0];
|
|
2362
|
+
})();
|
|
2363
|
+
return {
|
|
2364
|
+
isArray,
|
|
2365
|
+
nestedArray,
|
|
2366
|
+
rawCount,
|
|
2367
|
+
flattenedCount,
|
|
2368
|
+
sampleType: firstSample ? typeof firstSample : "undefined"
|
|
2369
|
+
};
|
|
2370
|
+
};
|
|
2265
2371
|
var normalizeQueryParamList = (value) => {
|
|
2266
2372
|
if (value === void 0 || value === null) {
|
|
2267
2373
|
return void 0;
|
|
@@ -2304,9 +2410,28 @@ var transformProduct = (perspectProduct, logger) => {
|
|
|
2304
2410
|
...rawProduct
|
|
2305
2411
|
};
|
|
2306
2412
|
};
|
|
2307
|
-
var transformContent = (perspectContent) => {
|
|
2413
|
+
var transformContent = (perspectContent, logger) => {
|
|
2308
2414
|
const raw = perspectContent ?? {};
|
|
2415
|
+
const mediaItems = normalizeMediaList(raw.media);
|
|
2416
|
+
const image = raw.image;
|
|
2417
|
+
if (raw.media !== void 0) {
|
|
2418
|
+
const mediaDetails = describeMedia(raw.media);
|
|
2419
|
+
const slugOrId = raw.slug ?? raw.id;
|
|
2420
|
+
if (mediaItems.length === 0 && (mediaDetails.rawCount > 0 || mediaDetails.flattenedCount > 0)) {
|
|
2421
|
+
log(logger, "debug", "[PerspectAPI] Content media present but failed normalization", {
|
|
2422
|
+
slug: slugOrId,
|
|
2423
|
+
...mediaDetails
|
|
2424
|
+
});
|
|
2425
|
+
} else {
|
|
2426
|
+
log(logger, "debug", "[PerspectAPI] Content media normalization", {
|
|
2427
|
+
slug: slugOrId,
|
|
2428
|
+
...mediaDetails,
|
|
2429
|
+
normalizedCount: mediaItems.length
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2309
2433
|
return {
|
|
2434
|
+
...raw,
|
|
2310
2435
|
id: typeof raw.id === "string" ? raw.id : String(raw.id ?? ""),
|
|
2311
2436
|
slug: raw.slug ?? `post-${raw.id ?? "unknown"}`,
|
|
2312
2437
|
slug_prefix: raw.slug_prefix ?? (raw.pageType === "post" ? "blog" : "page"),
|
|
@@ -2321,7 +2446,8 @@ var transformContent = (perspectContent) => {
|
|
|
2321
2446
|
published_date: raw.published_date,
|
|
2322
2447
|
author: raw.author,
|
|
2323
2448
|
tags: raw.tags,
|
|
2324
|
-
image
|
|
2449
|
+
image,
|
|
2450
|
+
media: mediaItems
|
|
2325
2451
|
};
|
|
2326
2452
|
};
|
|
2327
2453
|
var getDefaultFallbackProducts = (siteName) => {
|
|
@@ -2507,7 +2633,7 @@ async function loadPages(options) {
|
|
|
2507
2633
|
if (!response.data) {
|
|
2508
2634
|
return [];
|
|
2509
2635
|
}
|
|
2510
|
-
return response.data.map((content) => transformContent(content));
|
|
2636
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
2511
2637
|
} catch (error) {
|
|
2512
2638
|
log(logger, "error", "[PerspectAPI] Error loading pages", error);
|
|
2513
2639
|
return [];
|
|
@@ -2541,7 +2667,7 @@ async function loadPosts(options) {
|
|
|
2541
2667
|
log(logger, "warn", "[PerspectAPI] Posts response missing data");
|
|
2542
2668
|
return [];
|
|
2543
2669
|
}
|
|
2544
|
-
return response.data.map((content) => transformContent(content));
|
|
2670
|
+
return response.data.map((content) => transformContent(content, logger));
|
|
2545
2671
|
} catch (error) {
|
|
2546
2672
|
log(logger, "error", "[PerspectAPI] Error loading posts", error);
|
|
2547
2673
|
return [];
|
|
@@ -2567,7 +2693,7 @@ async function loadContentBySlug(options) {
|
|
|
2567
2693
|
log(logger, "warn", `[PerspectAPI] Content not found for slug "${slug}"`);
|
|
2568
2694
|
return null;
|
|
2569
2695
|
}
|
|
2570
|
-
return transformContent(response.data);
|
|
2696
|
+
return transformContent(response.data, logger);
|
|
2571
2697
|
} catch (error) {
|
|
2572
2698
|
log(logger, "error", `[PerspectAPI] Error loading content slug "${slug}"`, error);
|
|
2573
2699
|
return null;
|
package/package.json
CHANGED
|
@@ -76,13 +76,13 @@ export class ContentClient extends BaseClient {
|
|
|
76
76
|
console.log('[PerspectAPI Content] Cache details:', {
|
|
77
77
|
cacheEnabled: !!this.cache,
|
|
78
78
|
cachePolicy,
|
|
79
|
-
tags: this.buildContentTags(siteName)
|
|
79
|
+
tags: this.buildContentTags(siteName, undefined, undefined, normalizedParams?.slug_prefix)
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
const result = await this.fetchWithCache<PaginatedResponse<Content>>(
|
|
83
83
|
endpoint,
|
|
84
84
|
normalizedParams,
|
|
85
|
-
this.buildContentTags(siteName),
|
|
85
|
+
this.buildContentTags(siteName, undefined, undefined, normalizedParams?.slug_prefix),
|
|
86
86
|
cachePolicy,
|
|
87
87
|
async () => {
|
|
88
88
|
console.log('[PerspectAPI Content] Making HTTP request:', {
|
|
@@ -229,7 +229,7 @@ export class ContentClient extends BaseClient {
|
|
|
229
229
|
return this.create<Record<string, never>, Content>(`/content/${id}/duplicate`, {});
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
private buildContentTags(siteName?: string, slug?: string, id?: number): string[] {
|
|
232
|
+
private buildContentTags(siteName?: string, slug?: string, id?: number, slugPrefix?: string): string[] {
|
|
233
233
|
const tags = new Set<string>(['content']);
|
|
234
234
|
if (siteName) {
|
|
235
235
|
tags.add(`content:site:${siteName}`);
|
|
@@ -240,6 +240,9 @@ export class ContentClient extends BaseClient {
|
|
|
240
240
|
if (typeof id === 'number') {
|
|
241
241
|
tags.add(`content:id:${id}`);
|
|
242
242
|
}
|
|
243
|
+
if (slugPrefix) {
|
|
244
|
+
tags.add(`content:prefix:${slugPrefix}`);
|
|
245
|
+
}
|
|
243
246
|
return Array.from(tags.values());
|
|
244
247
|
}
|
|
245
248
|
}
|
|
@@ -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;
|