perspectapi-ts-sdk 2.5.0 → 2.7.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/README.md CHANGED
@@ -147,7 +147,7 @@ export default {
147
147
  };
148
148
  ```
149
149
 
150
- When PerspectAPI sends a webhook—or when your Worker mutates data directly—call `perspect.cache.invalidate({ tags: [...] })` using the tags emitted by the SDK (`products:site:<site>`, `content:slug:<site>:<slug>`, etc.) so stale entries are purged immediately.
150
+ When PerspectAPI sends a webhook—or when your Worker mutates data directly—call `perspect.cache.invalidate({ tags: [...] })` using the tags emitted by the SDK (`products:site:<site>`, `content:slug:<site>:<slug>`, `content:category:<site>:<category_slug>`, etc.) so stale entries are purged immediately.
151
151
 
152
152
  ### Webhook-driven cache invalidation
153
153
 
@@ -200,6 +200,8 @@ const tagMap: Record<string, (e: WebhookEvent) => string[]> = {
200
200
  `categories`,
201
201
  `categories:site:${event.site}`,
202
202
  `categories:product:${event.site}:${event.slug}`,
203
+ `products:category:${event.site}:${event.slug}`,
204
+ `content:category:${event.site}:${event.slug}`,
203
205
  ],
204
206
  };
205
207
 
@@ -392,6 +394,17 @@ const post = await client.content.getContentById(123);
392
394
  // Get content by slug
393
395
  const page = await client.content.getContentBySlug('your-site-name', 'about-us');
394
396
 
397
+ // Get content by category slug
398
+ const categoryContent = await client.content.getContentByCategorySlug(
399
+ 'your-site-name',
400
+ 'news',
401
+ {
402
+ page: 1,
403
+ limit: 20,
404
+ page_type: 'post'
405
+ }
406
+ );
407
+
395
408
  // Create new content
396
409
  const newPost = await client.content.createContent({
397
410
  page_title: 'My New Post',
package/dist/index.d.mts CHANGED
@@ -312,6 +312,22 @@ interface Category {
312
312
  createdAt: string;
313
313
  updatedAt: string;
314
314
  }
315
+ interface CategorySummary {
316
+ id: number;
317
+ name: string;
318
+ slug: string;
319
+ description?: string;
320
+ }
321
+ interface ContentCategoryResponse {
322
+ category: CategorySummary;
323
+ items: Content[];
324
+ pagination: {
325
+ page: number;
326
+ limit: number;
327
+ total: number;
328
+ pages: number;
329
+ };
330
+ }
315
331
  interface CreateCategoryRequest {
316
332
  name: string;
317
333
  slug?: string;
@@ -781,6 +797,10 @@ declare class ContentClient extends BaseClient {
781
797
  * Get content by ID
782
798
  */
783
799
  getContentById(id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<Content>>;
800
+ /**
801
+ * Get content by category slug for a site
802
+ */
803
+ getContentByCategorySlug(siteName: string, categorySlug: string, params?: ContentQueryParams, cachePolicy?: CachePolicy): Promise<ApiResponse<ContentCategoryResponse>>;
784
804
  /**
785
805
  * Get content by slug for a site
786
806
  */
@@ -828,6 +848,11 @@ declare class ContentClient extends BaseClient {
828
848
  */
829
849
  duplicateContent(id: number): Promise<ApiResponse<Content>>;
830
850
  private buildContentTags;
851
+ /**
852
+ * Extract the prefix from a slug (first segment before '/')
853
+ * e.g., 'blog/my-post' -> 'blog', 'about' -> undefined
854
+ */
855
+ private extractSlugPrefix;
831
856
  }
832
857
 
833
858
  /**
@@ -1226,6 +1251,11 @@ declare class ProductsClient extends BaseClient {
1226
1251
  };
1227
1252
  }>>;
1228
1253
  private buildProductTags;
1254
+ /**
1255
+ * Extract the prefix from a slug (first segment before '/')
1256
+ * e.g., 'shop/shoes/nike-air' -> 'shop', 'product-name' -> undefined
1257
+ */
1258
+ private extractSlugPrefix;
1229
1259
  }
1230
1260
 
1231
1261
  /**
@@ -2442,4 +2472,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2442
2472
  error: string;
2443
2473
  }>;
2444
2474
 
2445
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
2475
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
package/dist/index.d.ts CHANGED
@@ -312,6 +312,22 @@ interface Category {
312
312
  createdAt: string;
313
313
  updatedAt: string;
314
314
  }
315
+ interface CategorySummary {
316
+ id: number;
317
+ name: string;
318
+ slug: string;
319
+ description?: string;
320
+ }
321
+ interface ContentCategoryResponse {
322
+ category: CategorySummary;
323
+ items: Content[];
324
+ pagination: {
325
+ page: number;
326
+ limit: number;
327
+ total: number;
328
+ pages: number;
329
+ };
330
+ }
315
331
  interface CreateCategoryRequest {
316
332
  name: string;
317
333
  slug?: string;
@@ -781,6 +797,10 @@ declare class ContentClient extends BaseClient {
781
797
  * Get content by ID
782
798
  */
783
799
  getContentById(id: number, cachePolicy?: CachePolicy): Promise<ApiResponse<Content>>;
800
+ /**
801
+ * Get content by category slug for a site
802
+ */
803
+ getContentByCategorySlug(siteName: string, categorySlug: string, params?: ContentQueryParams, cachePolicy?: CachePolicy): Promise<ApiResponse<ContentCategoryResponse>>;
784
804
  /**
785
805
  * Get content by slug for a site
786
806
  */
@@ -828,6 +848,11 @@ declare class ContentClient extends BaseClient {
828
848
  */
829
849
  duplicateContent(id: number): Promise<ApiResponse<Content>>;
830
850
  private buildContentTags;
851
+ /**
852
+ * Extract the prefix from a slug (first segment before '/')
853
+ * e.g., 'blog/my-post' -> 'blog', 'about' -> undefined
854
+ */
855
+ private extractSlugPrefix;
831
856
  }
832
857
 
833
858
  /**
@@ -1226,6 +1251,11 @@ declare class ProductsClient extends BaseClient {
1226
1251
  };
1227
1252
  }>>;
1228
1253
  private buildProductTags;
1254
+ /**
1255
+ * Extract the prefix from a slug (first segment before '/')
1256
+ * e.g., 'shop/shoes/nike-air' -> 'shop', 'product-name' -> undefined
1257
+ */
1258
+ private extractSlugPrefix;
1229
1259
  }
1230
1260
 
1231
1261
  /**
@@ -2442,4 +2472,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2442
2472
  error: string;
2443
2473
  }>;
2444
2474
 
2445
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
2475
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, type CacheConfig, CacheManager, CategoriesClient, type Category, type CategorySummary, type CheckoutAddress, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, type CheckoutSessionTax, type CheckoutTaxBreakdownItem, type CheckoutTaxCustomerExemptionRequest, type CheckoutTaxExemptionStatus, type CheckoutTaxRequest, type CheckoutTaxStrategy, ContactClient, type ContactStatusResponse, type ContactSubmission, type ContactSubmitResponse, type Content, type ContentCategoryResponse, ContentClient, type ContentQueryParams, type ContentStatus, type ContentType, type CreateApiKeyRequest, type CreateCategoryRequest, type CreateCheckoutSessionRequest, type CreateContactRequest, type CreateContentRequest, type CreateNewsletterSubscriptionRequest, type CreateOrganizationRequest, type CreatePaymentGatewayRequest, type CreateProductRequest, type CreateSiteRequest, type CreateWebhookRequest, DEFAULT_IMAGE_SIZES, HttpClient, type HttpMethod, type ImageTransformOptions, InMemoryCacheAdapter, type LoadContentBySlugOptions, type LoadContentOptions, type LoadProductBySlugOptions, type LoadProductsOptions, type LoaderLogger, type LoaderOptions, type MediaItem, NewsletterClient, type NewsletterConfirmResponse, type NewsletterList, type NewsletterPreferences, type NewsletterStatusResponse, type NewsletterSubscribeResponse, type NewsletterSubscription, type NewsletterUnsubscribeRequest, type NewsletterUnsubscribeResponse, NoopCacheAdapter, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type ResponsiveImageSizes, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, buildImageUrl, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, generateResponsiveImageHtml, generateResponsiveUrls, generateSizesAttribute, generateSrcSet, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformMediaItem, transformProduct };
package/dist/index.js CHANGED
@@ -902,16 +902,61 @@ var ContentClient = class extends BaseClient {
902
902
  () => this.http.get(path)
903
903
  );
904
904
  }
905
+ /**
906
+ * Get content by category slug for a site
907
+ */
908
+ async getContentByCategorySlug(siteName, categorySlug, params, cachePolicy) {
909
+ const endpoint = this.siteScopedEndpoint(
910
+ siteName,
911
+ `/category/${encodeURIComponent(categorySlug)}`
912
+ );
913
+ const path = this.buildPath(endpoint);
914
+ const normalizedParams = params ? { ...params } : void 0;
915
+ if (normalizedParams) {
916
+ const validatedLimit = validateOptionalLimit(
917
+ normalizedParams.limit,
918
+ "content category query"
919
+ );
920
+ if (validatedLimit !== void 0) {
921
+ normalizedParams.limit = validatedLimit;
922
+ } else {
923
+ delete normalizedParams.limit;
924
+ }
925
+ const validatedPageType = validateOptionalContentType(
926
+ normalizedParams.page_type,
927
+ "content category query"
928
+ );
929
+ if (validatedPageType !== void 0) {
930
+ normalizedParams.page_type = validatedPageType;
931
+ } else {
932
+ delete normalizedParams.page_type;
933
+ }
934
+ }
935
+ return this.fetchWithCache(
936
+ endpoint,
937
+ normalizedParams,
938
+ this.buildContentTags(
939
+ siteName,
940
+ void 0,
941
+ void 0,
942
+ normalizedParams?.slug_prefix,
943
+ categorySlug
944
+ ),
945
+ cachePolicy,
946
+ () => this.http.get(path, normalizedParams)
947
+ );
948
+ }
905
949
  /**
906
950
  * Get content by slug for a site
907
951
  */
908
952
  async getContentBySlug(siteName, slug, cachePolicy) {
909
953
  const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
910
954
  const path = this.buildPath(endpoint);
955
+ const slugPrefix = this.extractSlugPrefix(slug);
911
956
  return this.fetchWithCache(
912
957
  endpoint,
913
958
  void 0,
914
- this.buildContentTags(siteName, slug),
959
+ this.buildContentTags(siteName, slug, void 0, slugPrefix),
915
960
  cachePolicy,
916
961
  () => this.http.get(path)
917
962
  );
@@ -976,7 +1021,7 @@ var ContentClient = class extends BaseClient {
976
1021
  async duplicateContent(id) {
977
1022
  return this.create(`/content/${id}/duplicate`, {});
978
1023
  }
979
- buildContentTags(siteName, slug, id, slugPrefix) {
1024
+ buildContentTags(siteName, slug, id, slugPrefix, categorySlug) {
980
1025
  const tags = /* @__PURE__ */ new Set(["content"]);
981
1026
  if (siteName) {
982
1027
  tags.add(`content:site:${siteName}`);
@@ -990,8 +1035,23 @@ var ContentClient = class extends BaseClient {
990
1035
  if (slugPrefix) {
991
1036
  tags.add(`content:prefix:${slugPrefix}`);
992
1037
  }
1038
+ if (categorySlug && siteName) {
1039
+ tags.add("content:category");
1040
+ tags.add(`content:category:${siteName}:${categorySlug}`);
1041
+ }
993
1042
  return Array.from(tags.values());
994
1043
  }
1044
+ /**
1045
+ * Extract the prefix from a slug (first segment before '/')
1046
+ * e.g., 'blog/my-post' -> 'blog', 'about' -> undefined
1047
+ */
1048
+ extractSlugPrefix(slug) {
1049
+ const slashIndex = slug.indexOf("/");
1050
+ if (slashIndex > 0) {
1051
+ return slug.substring(0, slashIndex);
1052
+ }
1053
+ return void 0;
1054
+ }
995
1055
  };
996
1056
 
997
1057
  // src/client/api-keys-client.ts
@@ -1277,7 +1337,7 @@ var ProductsClient = class extends BaseClient {
1277
1337
  return this.fetchWithCache(
1278
1338
  endpoint,
1279
1339
  normalizedParams,
1280
- this.buildProductTags(siteName, ["products:list"]),
1340
+ this.buildProductTags(siteName, ["products:list"], normalizedParams?.slug_prefix),
1281
1341
  cachePolicy,
1282
1342
  () => this.http.get(path, normalizedParams)
1283
1343
  );
@@ -1320,10 +1380,11 @@ var ProductsClient = class extends BaseClient {
1320
1380
  { includeSitesSegment: false }
1321
1381
  );
1322
1382
  const path = this.buildPath(endpoint);
1383
+ const slugPrefix = this.extractSlugPrefix(slug);
1323
1384
  return this.fetchWithCache(
1324
1385
  endpoint,
1325
1386
  void 0,
1326
- this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1387
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`], slugPrefix),
1327
1388
  cachePolicy,
1328
1389
  () => this.http.get(path)
1329
1390
  );
@@ -1439,14 +1500,28 @@ var ProductsClient = class extends BaseClient {
1439
1500
  () => this.http.get(path, queryParams)
1440
1501
  );
1441
1502
  }
1442
- buildProductTags(siteName, extraTags = []) {
1503
+ buildProductTags(siteName, extraTags = [], slugPrefix) {
1443
1504
  const tags = /* @__PURE__ */ new Set(["products"]);
1444
1505
  if (siteName) {
1445
1506
  tags.add(`products:site:${siteName}`);
1446
1507
  }
1508
+ if (slugPrefix) {
1509
+ tags.add(`products:prefix:${slugPrefix}`);
1510
+ }
1447
1511
  extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1448
1512
  return Array.from(tags.values());
1449
1513
  }
1514
+ /**
1515
+ * Extract the prefix from a slug (first segment before '/')
1516
+ * e.g., 'shop/shoes/nike-air' -> 'shop', 'product-name' -> undefined
1517
+ */
1518
+ extractSlugPrefix(slug) {
1519
+ const slashIndex = slug.indexOf("/");
1520
+ if (slashIndex > 0) {
1521
+ return slug.substring(0, slashIndex);
1522
+ }
1523
+ return void 0;
1524
+ }
1450
1525
  };
1451
1526
 
1452
1527
  // src/client/categories-client.ts
package/dist/index.mjs CHANGED
@@ -841,16 +841,61 @@ var ContentClient = class extends BaseClient {
841
841
  () => this.http.get(path)
842
842
  );
843
843
  }
844
+ /**
845
+ * Get content by category slug for a site
846
+ */
847
+ async getContentByCategorySlug(siteName, categorySlug, params, cachePolicy) {
848
+ const endpoint = this.siteScopedEndpoint(
849
+ siteName,
850
+ `/category/${encodeURIComponent(categorySlug)}`
851
+ );
852
+ const path = this.buildPath(endpoint);
853
+ const normalizedParams = params ? { ...params } : void 0;
854
+ if (normalizedParams) {
855
+ const validatedLimit = validateOptionalLimit(
856
+ normalizedParams.limit,
857
+ "content category query"
858
+ );
859
+ if (validatedLimit !== void 0) {
860
+ normalizedParams.limit = validatedLimit;
861
+ } else {
862
+ delete normalizedParams.limit;
863
+ }
864
+ const validatedPageType = validateOptionalContentType(
865
+ normalizedParams.page_type,
866
+ "content category query"
867
+ );
868
+ if (validatedPageType !== void 0) {
869
+ normalizedParams.page_type = validatedPageType;
870
+ } else {
871
+ delete normalizedParams.page_type;
872
+ }
873
+ }
874
+ return this.fetchWithCache(
875
+ endpoint,
876
+ normalizedParams,
877
+ this.buildContentTags(
878
+ siteName,
879
+ void 0,
880
+ void 0,
881
+ normalizedParams?.slug_prefix,
882
+ categorySlug
883
+ ),
884
+ cachePolicy,
885
+ () => this.http.get(path, normalizedParams)
886
+ );
887
+ }
844
888
  /**
845
889
  * Get content by slug for a site
846
890
  */
847
891
  async getContentBySlug(siteName, slug, cachePolicy) {
848
892
  const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
849
893
  const path = this.buildPath(endpoint);
894
+ const slugPrefix = this.extractSlugPrefix(slug);
850
895
  return this.fetchWithCache(
851
896
  endpoint,
852
897
  void 0,
853
- this.buildContentTags(siteName, slug),
898
+ this.buildContentTags(siteName, slug, void 0, slugPrefix),
854
899
  cachePolicy,
855
900
  () => this.http.get(path)
856
901
  );
@@ -915,7 +960,7 @@ var ContentClient = class extends BaseClient {
915
960
  async duplicateContent(id) {
916
961
  return this.create(`/content/${id}/duplicate`, {});
917
962
  }
918
- buildContentTags(siteName, slug, id, slugPrefix) {
963
+ buildContentTags(siteName, slug, id, slugPrefix, categorySlug) {
919
964
  const tags = /* @__PURE__ */ new Set(["content"]);
920
965
  if (siteName) {
921
966
  tags.add(`content:site:${siteName}`);
@@ -929,8 +974,23 @@ var ContentClient = class extends BaseClient {
929
974
  if (slugPrefix) {
930
975
  tags.add(`content:prefix:${slugPrefix}`);
931
976
  }
977
+ if (categorySlug && siteName) {
978
+ tags.add("content:category");
979
+ tags.add(`content:category:${siteName}:${categorySlug}`);
980
+ }
932
981
  return Array.from(tags.values());
933
982
  }
983
+ /**
984
+ * Extract the prefix from a slug (first segment before '/')
985
+ * e.g., 'blog/my-post' -> 'blog', 'about' -> undefined
986
+ */
987
+ extractSlugPrefix(slug) {
988
+ const slashIndex = slug.indexOf("/");
989
+ if (slashIndex > 0) {
990
+ return slug.substring(0, slashIndex);
991
+ }
992
+ return void 0;
993
+ }
934
994
  };
935
995
 
936
996
  // src/client/api-keys-client.ts
@@ -1216,7 +1276,7 @@ var ProductsClient = class extends BaseClient {
1216
1276
  return this.fetchWithCache(
1217
1277
  endpoint,
1218
1278
  normalizedParams,
1219
- this.buildProductTags(siteName, ["products:list"]),
1279
+ this.buildProductTags(siteName, ["products:list"], normalizedParams?.slug_prefix),
1220
1280
  cachePolicy,
1221
1281
  () => this.http.get(path, normalizedParams)
1222
1282
  );
@@ -1259,10 +1319,11 @@ var ProductsClient = class extends BaseClient {
1259
1319
  { includeSitesSegment: false }
1260
1320
  );
1261
1321
  const path = this.buildPath(endpoint);
1322
+ const slugPrefix = this.extractSlugPrefix(slug);
1262
1323
  return this.fetchWithCache(
1263
1324
  endpoint,
1264
1325
  void 0,
1265
- this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1326
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`], slugPrefix),
1266
1327
  cachePolicy,
1267
1328
  () => this.http.get(path)
1268
1329
  );
@@ -1378,14 +1439,28 @@ var ProductsClient = class extends BaseClient {
1378
1439
  () => this.http.get(path, queryParams)
1379
1440
  );
1380
1441
  }
1381
- buildProductTags(siteName, extraTags = []) {
1442
+ buildProductTags(siteName, extraTags = [], slugPrefix) {
1382
1443
  const tags = /* @__PURE__ */ new Set(["products"]);
1383
1444
  if (siteName) {
1384
1445
  tags.add(`products:site:${siteName}`);
1385
1446
  }
1447
+ if (slugPrefix) {
1448
+ tags.add(`products:prefix:${slugPrefix}`);
1449
+ }
1386
1450
  extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1387
1451
  return Array.from(tags.values());
1388
1452
  }
1453
+ /**
1454
+ * Extract the prefix from a slug (first segment before '/')
1455
+ * e.g., 'shop/shoes/nike-air' -> 'shop', 'product-name' -> undefined
1456
+ */
1457
+ extractSlugPrefix(slug) {
1458
+ const slashIndex = slug.indexOf("/");
1459
+ if (slashIndex > 0) {
1460
+ return slug.substring(0, slashIndex);
1461
+ }
1462
+ return void 0;
1463
+ }
1389
1464
  };
1390
1465
 
1391
1466
  // src/client/categories-client.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -12,6 +12,7 @@ import type {
12
12
  ContentQueryParams,
13
13
  PaginatedResponse,
14
14
  ApiResponse,
15
+ ContentCategoryResponse,
15
16
  } from '../types';
16
17
  import { validateOptionalContentType, validateOptionalLimit } from '../utils/validators';
17
18
 
@@ -139,6 +140,61 @@ export class ContentClient extends BaseClient {
139
140
  );
140
141
  }
141
142
 
143
+ /**
144
+ * Get content by category slug for a site
145
+ */
146
+ async getContentByCategorySlug(
147
+ siteName: string,
148
+ categorySlug: string,
149
+ params?: ContentQueryParams,
150
+ cachePolicy?: CachePolicy
151
+ ): Promise<ApiResponse<ContentCategoryResponse>> {
152
+ const endpoint = this.siteScopedEndpoint(
153
+ siteName,
154
+ `/category/${encodeURIComponent(categorySlug)}`
155
+ );
156
+ const path = this.buildPath(endpoint);
157
+ const normalizedParams: ContentQueryParams | undefined = params
158
+ ? { ...params }
159
+ : undefined;
160
+
161
+ if (normalizedParams) {
162
+ const validatedLimit = validateOptionalLimit(
163
+ normalizedParams.limit,
164
+ 'content category query',
165
+ );
166
+ if (validatedLimit !== undefined) {
167
+ normalizedParams.limit = validatedLimit;
168
+ } else {
169
+ delete normalizedParams.limit;
170
+ }
171
+
172
+ const validatedPageType = validateOptionalContentType(
173
+ normalizedParams.page_type,
174
+ 'content category query',
175
+ );
176
+ if (validatedPageType !== undefined) {
177
+ normalizedParams.page_type = validatedPageType;
178
+ } else {
179
+ delete normalizedParams.page_type;
180
+ }
181
+ }
182
+
183
+ return this.fetchWithCache<ApiResponse<ContentCategoryResponse>>(
184
+ endpoint,
185
+ normalizedParams,
186
+ this.buildContentTags(
187
+ siteName,
188
+ undefined,
189
+ undefined,
190
+ normalizedParams?.slug_prefix,
191
+ categorySlug
192
+ ),
193
+ cachePolicy,
194
+ () => this.http.get<ContentCategoryResponse>(path, normalizedParams)
195
+ );
196
+ }
197
+
142
198
  /**
143
199
  * Get content by slug for a site
144
200
  */
@@ -149,11 +205,12 @@ export class ContentClient extends BaseClient {
149
205
  ): Promise<ApiResponse<Content>> {
150
206
  const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
151
207
  const path = this.buildPath(endpoint);
208
+ const slugPrefix = this.extractSlugPrefix(slug);
152
209
 
153
210
  return this.fetchWithCache<ApiResponse<Content>>(
154
211
  endpoint,
155
212
  undefined,
156
- this.buildContentTags(siteName, slug),
213
+ this.buildContentTags(siteName, slug, undefined, slugPrefix),
157
214
  cachePolicy,
158
215
  () => this.http.get<Content>(path)
159
216
  );
@@ -229,7 +286,13 @@ export class ContentClient extends BaseClient {
229
286
  return this.create<Record<string, never>, Content>(`/content/${id}/duplicate`, {});
230
287
  }
231
288
 
232
- private buildContentTags(siteName?: string, slug?: string, id?: number, slugPrefix?: string): string[] {
289
+ private buildContentTags(
290
+ siteName?: string,
291
+ slug?: string,
292
+ id?: number,
293
+ slugPrefix?: string,
294
+ categorySlug?: string
295
+ ): string[] {
233
296
  const tags = new Set<string>(['content']);
234
297
  if (siteName) {
235
298
  tags.add(`content:site:${siteName}`);
@@ -243,6 +306,22 @@ export class ContentClient extends BaseClient {
243
306
  if (slugPrefix) {
244
307
  tags.add(`content:prefix:${slugPrefix}`);
245
308
  }
309
+ if (categorySlug && siteName) {
310
+ tags.add('content:category');
311
+ tags.add(`content:category:${siteName}:${categorySlug}`);
312
+ }
246
313
  return Array.from(tags.values());
247
314
  }
315
+
316
+ /**
317
+ * Extract the prefix from a slug (first segment before '/')
318
+ * e.g., 'blog/my-post' -> 'blog', 'about' -> undefined
319
+ */
320
+ private extractSlugPrefix(slug: string): string | undefined {
321
+ const slashIndex = slug.indexOf('/');
322
+ if (slashIndex > 0) {
323
+ return slug.substring(0, slashIndex);
324
+ }
325
+ return undefined;
326
+ }
248
327
  }
@@ -73,7 +73,7 @@ export class ProductsClient extends BaseClient {
73
73
  return this.fetchWithCache<PaginatedResponse<Product>>(
74
74
  endpoint,
75
75
  normalizedParams,
76
- this.buildProductTags(siteName, ['products:list']),
76
+ this.buildProductTags(siteName, ['products:list'], normalizedParams?.slug_prefix),
77
77
  cachePolicy,
78
78
  () => this.http.get(path, normalizedParams) as Promise<PaginatedResponse<Product>>
79
79
  );
@@ -125,11 +125,12 @@ export class ProductsClient extends BaseClient {
125
125
  { includeSitesSegment: false }
126
126
  );
127
127
  const path = this.buildPath(endpoint);
128
+ const slugPrefix = this.extractSlugPrefix(slug);
128
129
 
129
130
  return this.fetchWithCache<ApiResponse<Product & { variants?: any[] }>>(
130
131
  endpoint,
131
132
  undefined,
132
- this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
133
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`], slugPrefix),
133
134
  cachePolicy,
134
135
  () => this.http.get<Product & { variants?: any[] }>(path)
135
136
  );
@@ -347,12 +348,27 @@ export class ProductsClient extends BaseClient {
347
348
  );
348
349
  }
349
350
 
350
- private buildProductTags(siteName?: string, extraTags: string[] = []): string[] {
351
+ private buildProductTags(siteName?: string, extraTags: string[] = [], slugPrefix?: string): string[] {
351
352
  const tags = new Set<string>(['products']);
352
353
  if (siteName) {
353
354
  tags.add(`products:site:${siteName}`);
354
355
  }
356
+ if (slugPrefix) {
357
+ tags.add(`products:prefix:${slugPrefix}`);
358
+ }
355
359
  extraTags.filter(Boolean).forEach(tag => tags.add(tag));
356
360
  return Array.from(tags.values());
357
361
  }
362
+
363
+ /**
364
+ * Extract the prefix from a slug (first segment before '/')
365
+ * e.g., 'shop/shoes/nike-air' -> 'shop', 'product-name' -> undefined
366
+ */
367
+ private extractSlugPrefix(slug: string): string | undefined {
368
+ const slashIndex = slug.indexOf('/');
369
+ if (slashIndex > 0) {
370
+ return slug.substring(0, slashIndex);
371
+ }
372
+ return undefined;
373
+ }
358
374
  }
package/src/index.ts CHANGED
@@ -80,12 +80,14 @@ export type {
80
80
  PaginatedResponse,
81
81
  ApiError,
82
82
  User,
83
- Content,
83
+ Content,
84
+ ContentCategoryResponse,
84
85
  Product,
85
86
  ProductQueryParams,
86
87
  MediaItem,
87
88
  BlogPost,
88
89
  Category,
90
+ CategorySummary,
89
91
  Organization,
90
92
  Site,
91
93
  ApiKey,
@@ -268,6 +268,24 @@ export interface Category {
268
268
  updatedAt: string;
269
269
  }
270
270
 
271
+ export interface CategorySummary {
272
+ id: number;
273
+ name: string;
274
+ slug: string;
275
+ description?: string;
276
+ }
277
+
278
+ export interface ContentCategoryResponse {
279
+ category: CategorySummary;
280
+ items: Content[];
281
+ pagination: {
282
+ page: number;
283
+ limit: number;
284
+ total: number;
285
+ pages: number;
286
+ };
287
+ }
288
+
271
289
  export interface CreateCategoryRequest {
272
290
  name: string;
273
291
  slug?: string;