perspectapi-ts-sdk 1.4.1 → 1.5.1

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
@@ -84,6 +84,51 @@ const checkout = await createCheckoutSession({
84
84
  > 📚 See [docs/loaders.md](docs/loaders.md) for full walkthroughs, including fallback data, custom logging, and Stripe price resolution.
85
85
  ```
86
86
 
87
+ ## Image Transformations
88
+
89
+ The SDK includes utilities for transforming images using Cloudflare's Image Resizing service:
90
+
91
+ ```typescript
92
+ import { transformMediaItem, buildImageUrl } from 'perspectapi-ts-sdk';
93
+
94
+ // Get a product with media
95
+ const product = await client.products.getProduct('mysite', 123);
96
+ const media = product.data.media?.[0];
97
+
98
+ if (media) {
99
+ // Generate all responsive sizes automatically
100
+ const urls = transformMediaItem('https://api.perspect.co', media);
101
+
102
+ console.log(urls.thumbnail); // 150x150 cover crop
103
+ console.log(urls.small); // 400px wide
104
+ console.log(urls.medium); // 800px wide
105
+ console.log(urls.large); // 1200px wide
106
+ console.log(urls.original); // Original with auto format
107
+ }
108
+
109
+ // Or build custom transformations
110
+ const customUrl = buildImageUrl(
111
+ 'https://api.perspect.co',
112
+ 'media/mysite/photo.jpg',
113
+ {
114
+ width: 400,
115
+ height: 300,
116
+ fit: 'cover',
117
+ format: 'webp',
118
+ quality: 85
119
+ }
120
+ );
121
+ ```
122
+
123
+ **Features:**
124
+ - ✅ On-the-fly image resizing (no pre-generated thumbnails)
125
+ - ✅ Automatic format optimization (WebP/AVIF)
126
+ - ✅ Responsive image support with srcset
127
+ - ✅ CDN caching at the edge
128
+ - ✅ Bandwidth savings
129
+
130
+ > 📚 See [docs/image-transforms.md](docs/image-transforms.md) for complete documentation, examples, and best practices.
131
+
87
132
  ## High-Level Data Loaders
88
133
 
89
134
  The SDK now ships with convenience loaders that wrap the lower-level REST clients. They help you:
@@ -189,7 +234,8 @@ const content = await client.content.getContent('your-site-name', {
189
234
  page: 1,
190
235
  limit: 20,
191
236
  page_status: 'publish',
192
- page_type: 'post'
237
+ page_type: 'post',
238
+ slug_prefix: 'blog' // Optional: filter by slug prefix
193
239
  });
194
240
 
195
241
  // Get content by ID
@@ -229,7 +275,8 @@ await client.content.unpublishContent(123);
229
275
  const products = await client.products.getProducts('my-site', {
230
276
  page: 1,
231
277
  limit: 20,
232
- isActive: true
278
+ isActive: true,
279
+ slug_prefix: 'shop' // Optional: filter by slug prefix
233
280
  });
234
281
 
235
282
  // Create new product
@@ -605,6 +652,54 @@ const client = createPerspectApiClient({
605
652
  });
606
653
  ```
607
654
 
655
+ ## Slug Prefix Filtering
656
+
657
+ The SDK supports filtering content and products by `slug_prefix` to organize your content by URL structure:
658
+
659
+ ```typescript
660
+ // Filter blog posts
661
+ const blogPosts = await client.content.getContent('mysite', {
662
+ slug_prefix: 'blog', // /blog/post-1, /blog/post-2
663
+ page_status: 'publish'
664
+ });
665
+
666
+ // Filter news articles
667
+ const news = await client.content.getContent('mysite', {
668
+ slug_prefix: 'news', // /news/article-1, /news/article-2
669
+ page_status: 'publish'
670
+ });
671
+
672
+ // Filter shop products
673
+ const shopProducts = await client.products.getProducts('mysite', {
674
+ slug_prefix: 'shop', // /shop/product-1, /shop/product-2
675
+ isActive: true
676
+ });
677
+
678
+ // Filter artwork products
679
+ const artwork = await client.products.getProducts('mysite', {
680
+ slug_prefix: 'artwork', // /artwork/painting-1, /artwork/sculpture-1
681
+ isActive: true
682
+ });
683
+
684
+ // Combine with other filters
685
+ const results = await client.content.getContent('mysite', {
686
+ slug_prefix: 'blog',
687
+ page_status: 'publish',
688
+ page_type: 'post',
689
+ search: 'javascript', // Search within blog posts only
690
+ page: 1,
691
+ limit: 20
692
+ });
693
+ ```
694
+
695
+ **Use Cases:**
696
+ - Organize blog posts, news, and documentation separately
697
+ - Separate shop products from artwork or digital downloads
698
+ - Create multi-section websites with distinct URL structures
699
+ - Filter content/products by section for navigation
700
+
701
+ **See `examples/slug-prefix-examples.ts` for 20+ real-world examples!**
702
+
608
703
  ## TypeScript Support
609
704
 
610
705
  The SDK is written in TypeScript and provides comprehensive type definitions:
package/dist/index.d.mts CHANGED
@@ -188,6 +188,7 @@ interface Content {
188
188
  contentMarkdown?: string;
189
189
  custom?: Record<string, any>;
190
190
  slug?: string;
191
+ slug_prefix?: string;
191
192
  pageStatus: ContentStatus;
192
193
  pageType: ContentType;
193
194
  userId?: number;
@@ -211,6 +212,7 @@ interface ContentQueryParams extends PaginationParams {
211
212
  page_type?: ContentType;
212
213
  search?: string;
213
214
  user_id?: number;
215
+ slug_prefix?: string;
214
216
  }
215
217
  interface Category {
216
218
  id: number;
@@ -277,6 +279,7 @@ interface ProductQueryParams extends PaginationParams {
277
279
  search?: string;
278
280
  category?: string | string[];
279
281
  category_id?: number | string | Array<number | string>;
282
+ slug_prefix?: string;
280
283
  }
281
284
  interface BlogPost {
282
285
  id: string | number;
@@ -1925,6 +1928,145 @@ declare class PerspectApiClient {
1925
1928
  */
1926
1929
  declare function createPerspectApiClient(config: PerspectApiConfig): PerspectApiClient;
1927
1930
 
1931
+ /**
1932
+ * Cloudflare Image Resizing Integration
1933
+ * Transforms images on-the-fly using Cloudflare's Image Resizing service
1934
+ * https://developers.cloudflare.com/images/transform-images/transform-via-url/
1935
+ */
1936
+ interface ImageTransformOptions {
1937
+ width?: number;
1938
+ height?: number;
1939
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
1940
+ gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center';
1941
+ quality?: number;
1942
+ format?: 'auto' | 'avif' | 'webp' | 'json' | 'jpeg' | 'png';
1943
+ sharpen?: number;
1944
+ blur?: number;
1945
+ rotate?: 0 | 90 | 180 | 270;
1946
+ dpr?: number;
1947
+ metadata?: 'keep' | 'copyright' | 'none';
1948
+ background?: string;
1949
+ trim?: {
1950
+ top?: number;
1951
+ right?: number;
1952
+ bottom?: number;
1953
+ left?: number;
1954
+ };
1955
+ }
1956
+ interface ResponsiveImageSizes {
1957
+ thumbnail: ImageTransformOptions;
1958
+ small: ImageTransformOptions;
1959
+ medium: ImageTransformOptions;
1960
+ large: ImageTransformOptions;
1961
+ original: ImageTransformOptions;
1962
+ }
1963
+ /**
1964
+ * Default responsive image sizes
1965
+ */
1966
+ declare const DEFAULT_IMAGE_SIZES: ResponsiveImageSizes;
1967
+ /**
1968
+ * Build Cloudflare Image Resizing URL
1969
+ *
1970
+ * @param baseUrl - The base URL of your API (e.g., "https://api.perspect.co")
1971
+ * @param mediaPath - The path to the media file (e.g., "media/site/image.jpg")
1972
+ * @param options - Transform options
1973
+ * @returns Cloudflare Image Resizing URL
1974
+ *
1975
+ * @example
1976
+ * ```typescript
1977
+ * const url = buildImageUrl(
1978
+ * 'https://api.perspect.co',
1979
+ * 'media/mysite/photo.jpg',
1980
+ * { width: 400, format: 'webp', quality: 85 }
1981
+ * );
1982
+ * // Returns: '/cdn-cgi/image/width=400,format=webp,quality=85/https://api.perspect.co/media/mysite/photo.jpg'
1983
+ * ```
1984
+ */
1985
+ declare function buildImageUrl(baseUrl: string, mediaPath: string, options?: ImageTransformOptions): string;
1986
+ /**
1987
+ * Generate responsive image URLs for different sizes
1988
+ *
1989
+ * @example
1990
+ * ```typescript
1991
+ * const urls = generateResponsiveUrls(
1992
+ * 'https://api.perspect.co',
1993
+ * 'media/mysite/photo.jpg'
1994
+ * );
1995
+ * // Returns: { thumbnail: '...', small: '...', medium: '...', large: '...', original: '...' }
1996
+ * ```
1997
+ */
1998
+ declare function generateResponsiveUrls(baseUrl: string, mediaPath: string, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
1999
+ /**
2000
+ * Generate srcset for responsive images
2001
+ *
2002
+ * @example
2003
+ * ```typescript
2004
+ * const srcset = generateSrcSet(
2005
+ * 'https://api.perspect.co',
2006
+ * 'media/mysite/photo.jpg',
2007
+ * [400, 800, 1200]
2008
+ * );
2009
+ * // Returns: "...?width=400 400w, ...?width=800 800w, ...?width=1200 1200w"
2010
+ * ```
2011
+ */
2012
+ declare function generateSrcSet(baseUrl: string, mediaPath: string, widths?: number[]): string;
2013
+ /**
2014
+ * Generate sizes attribute for responsive images
2015
+ *
2016
+ * @example
2017
+ * ```typescript
2018
+ * const sizes = generateSizesAttribute();
2019
+ * // Returns: "(max-width: 640px) 100vw, (max-width: 768px) 80vw, ..."
2020
+ * ```
2021
+ */
2022
+ declare function generateSizesAttribute(breakpoints?: Array<{
2023
+ maxWidth: string;
2024
+ size: string;
2025
+ }>): string;
2026
+ /**
2027
+ * Helper to generate complete responsive image HTML
2028
+ *
2029
+ * @example
2030
+ * ```typescript
2031
+ * const html = generateResponsiveImageHtml(
2032
+ * 'https://api.perspect.co',
2033
+ * 'media/mysite/photo.jpg',
2034
+ * 'My photo',
2035
+ * { className: 'rounded-lg', loading: 'lazy' }
2036
+ * );
2037
+ * ```
2038
+ */
2039
+ declare function generateResponsiveImageHtml(baseUrl: string, mediaPath: string, alt: string, options?: {
2040
+ className?: string;
2041
+ loading?: 'lazy' | 'eager';
2042
+ decoding?: 'async' | 'sync' | 'auto';
2043
+ sizes?: string;
2044
+ widths?: number[];
2045
+ }): string;
2046
+ /**
2047
+ * Transform a MediaItem into responsive URLs
2048
+ * Convenience function for working with MediaItem objects from the API
2049
+ *
2050
+ * @example
2051
+ * ```typescript
2052
+ * import { transformMediaItem } from 'perspectapi-ts-sdk';
2053
+ *
2054
+ * const product = await client.products.getProduct('mysite', 123);
2055
+ * const media = product.data.media?.[0];
2056
+ *
2057
+ * if (media) {
2058
+ * const urls = transformMediaItem('https://api.perspect.co', media);
2059
+ * console.log(urls.thumbnail); // Cloudflare-transformed thumbnail URL
2060
+ * }
2061
+ * ```
2062
+ */
2063
+ declare function transformMediaItem(baseUrl: string, media: {
2064
+ link: string;
2065
+ } | {
2066
+ r2_key: string;
2067
+ site_name: string;
2068
+ }, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
2069
+
1928
2070
  /**
1929
2071
  * High-level data loading helpers that wrap the PerspectAPI SDK clients.
1930
2072
  * These helpers provide convenient product and content loading utilities with
@@ -2057,4 +2199,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2057
2199
  error: string;
2058
2200
  }>;
2059
2201
 
2060
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, 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, HttpClient, type HttpMethod, 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, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformProduct };
2202
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, 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, 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, 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
@@ -188,6 +188,7 @@ interface Content {
188
188
  contentMarkdown?: string;
189
189
  custom?: Record<string, any>;
190
190
  slug?: string;
191
+ slug_prefix?: string;
191
192
  pageStatus: ContentStatus;
192
193
  pageType: ContentType;
193
194
  userId?: number;
@@ -211,6 +212,7 @@ interface ContentQueryParams extends PaginationParams {
211
212
  page_type?: ContentType;
212
213
  search?: string;
213
214
  user_id?: number;
215
+ slug_prefix?: string;
214
216
  }
215
217
  interface Category {
216
218
  id: number;
@@ -277,6 +279,7 @@ interface ProductQueryParams extends PaginationParams {
277
279
  search?: string;
278
280
  category?: string | string[];
279
281
  category_id?: number | string | Array<number | string>;
282
+ slug_prefix?: string;
280
283
  }
281
284
  interface BlogPost {
282
285
  id: string | number;
@@ -1925,6 +1928,145 @@ declare class PerspectApiClient {
1925
1928
  */
1926
1929
  declare function createPerspectApiClient(config: PerspectApiConfig): PerspectApiClient;
1927
1930
 
1931
+ /**
1932
+ * Cloudflare Image Resizing Integration
1933
+ * Transforms images on-the-fly using Cloudflare's Image Resizing service
1934
+ * https://developers.cloudflare.com/images/transform-images/transform-via-url/
1935
+ */
1936
+ interface ImageTransformOptions {
1937
+ width?: number;
1938
+ height?: number;
1939
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
1940
+ gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center';
1941
+ quality?: number;
1942
+ format?: 'auto' | 'avif' | 'webp' | 'json' | 'jpeg' | 'png';
1943
+ sharpen?: number;
1944
+ blur?: number;
1945
+ rotate?: 0 | 90 | 180 | 270;
1946
+ dpr?: number;
1947
+ metadata?: 'keep' | 'copyright' | 'none';
1948
+ background?: string;
1949
+ trim?: {
1950
+ top?: number;
1951
+ right?: number;
1952
+ bottom?: number;
1953
+ left?: number;
1954
+ };
1955
+ }
1956
+ interface ResponsiveImageSizes {
1957
+ thumbnail: ImageTransformOptions;
1958
+ small: ImageTransformOptions;
1959
+ medium: ImageTransformOptions;
1960
+ large: ImageTransformOptions;
1961
+ original: ImageTransformOptions;
1962
+ }
1963
+ /**
1964
+ * Default responsive image sizes
1965
+ */
1966
+ declare const DEFAULT_IMAGE_SIZES: ResponsiveImageSizes;
1967
+ /**
1968
+ * Build Cloudflare Image Resizing URL
1969
+ *
1970
+ * @param baseUrl - The base URL of your API (e.g., "https://api.perspect.co")
1971
+ * @param mediaPath - The path to the media file (e.g., "media/site/image.jpg")
1972
+ * @param options - Transform options
1973
+ * @returns Cloudflare Image Resizing URL
1974
+ *
1975
+ * @example
1976
+ * ```typescript
1977
+ * const url = buildImageUrl(
1978
+ * 'https://api.perspect.co',
1979
+ * 'media/mysite/photo.jpg',
1980
+ * { width: 400, format: 'webp', quality: 85 }
1981
+ * );
1982
+ * // Returns: '/cdn-cgi/image/width=400,format=webp,quality=85/https://api.perspect.co/media/mysite/photo.jpg'
1983
+ * ```
1984
+ */
1985
+ declare function buildImageUrl(baseUrl: string, mediaPath: string, options?: ImageTransformOptions): string;
1986
+ /**
1987
+ * Generate responsive image URLs for different sizes
1988
+ *
1989
+ * @example
1990
+ * ```typescript
1991
+ * const urls = generateResponsiveUrls(
1992
+ * 'https://api.perspect.co',
1993
+ * 'media/mysite/photo.jpg'
1994
+ * );
1995
+ * // Returns: { thumbnail: '...', small: '...', medium: '...', large: '...', original: '...' }
1996
+ * ```
1997
+ */
1998
+ declare function generateResponsiveUrls(baseUrl: string, mediaPath: string, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
1999
+ /**
2000
+ * Generate srcset for responsive images
2001
+ *
2002
+ * @example
2003
+ * ```typescript
2004
+ * const srcset = generateSrcSet(
2005
+ * 'https://api.perspect.co',
2006
+ * 'media/mysite/photo.jpg',
2007
+ * [400, 800, 1200]
2008
+ * );
2009
+ * // Returns: "...?width=400 400w, ...?width=800 800w, ...?width=1200 1200w"
2010
+ * ```
2011
+ */
2012
+ declare function generateSrcSet(baseUrl: string, mediaPath: string, widths?: number[]): string;
2013
+ /**
2014
+ * Generate sizes attribute for responsive images
2015
+ *
2016
+ * @example
2017
+ * ```typescript
2018
+ * const sizes = generateSizesAttribute();
2019
+ * // Returns: "(max-width: 640px) 100vw, (max-width: 768px) 80vw, ..."
2020
+ * ```
2021
+ */
2022
+ declare function generateSizesAttribute(breakpoints?: Array<{
2023
+ maxWidth: string;
2024
+ size: string;
2025
+ }>): string;
2026
+ /**
2027
+ * Helper to generate complete responsive image HTML
2028
+ *
2029
+ * @example
2030
+ * ```typescript
2031
+ * const html = generateResponsiveImageHtml(
2032
+ * 'https://api.perspect.co',
2033
+ * 'media/mysite/photo.jpg',
2034
+ * 'My photo',
2035
+ * { className: 'rounded-lg', loading: 'lazy' }
2036
+ * );
2037
+ * ```
2038
+ */
2039
+ declare function generateResponsiveImageHtml(baseUrl: string, mediaPath: string, alt: string, options?: {
2040
+ className?: string;
2041
+ loading?: 'lazy' | 'eager';
2042
+ decoding?: 'async' | 'sync' | 'auto';
2043
+ sizes?: string;
2044
+ widths?: number[];
2045
+ }): string;
2046
+ /**
2047
+ * Transform a MediaItem into responsive URLs
2048
+ * Convenience function for working with MediaItem objects from the API
2049
+ *
2050
+ * @example
2051
+ * ```typescript
2052
+ * import { transformMediaItem } from 'perspectapi-ts-sdk';
2053
+ *
2054
+ * const product = await client.products.getProduct('mysite', 123);
2055
+ * const media = product.data.media?.[0];
2056
+ *
2057
+ * if (media) {
2058
+ * const urls = transformMediaItem('https://api.perspect.co', media);
2059
+ * console.log(urls.thumbnail); // Cloudflare-transformed thumbnail URL
2060
+ * }
2061
+ * ```
2062
+ */
2063
+ declare function transformMediaItem(baseUrl: string, media: {
2064
+ link: string;
2065
+ } | {
2066
+ r2_key: string;
2067
+ site_name: string;
2068
+ }, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
2069
+
1928
2070
  /**
1929
2071
  * High-level data loading helpers that wrap the PerspectAPI SDK clients.
1930
2072
  * These helpers provide convenient product and content loading utilities with
@@ -2057,4 +2199,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2057
2199
  error: string;
2058
2200
  }>;
2059
2201
 
2060
- export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, 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, HttpClient, type HttpMethod, 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, type Organization, OrganizationsClient, type PaginatedResponse, type PaginationParams, type PaymentGateway, PerspectApiClient, type PerspectApiConfig, type Product, type ProductQueryParams, ProductsClient, type RequestOptions, type SignInRequest, type SignUpRequest, type Site, SitesClient, type UpdateApiKeyRequest, type UpdateContentRequest, type User, type Webhook, WebhooksClient, createApiError, createCheckoutSession, createPerspectApiClient, PerspectApiClient as default, loadAllContent, loadContentBySlug, loadPages, loadPosts, loadProductBySlug, loadProducts, transformContent, transformProduct };
2202
+ export { type ApiError, type ApiKey, ApiKeysClient, type ApiResponse, AuthClient, type AuthResponse, BaseClient, type BlogPost, CategoriesClient, type Category, CheckoutClient, type CheckoutMetadata, type CheckoutMetadataValue, type CheckoutSession, type CheckoutSessionOptions, 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, 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, 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
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  CheckoutClient: () => CheckoutClient,
28
28
  ContactClient: () => ContactClient,
29
29
  ContentClient: () => ContentClient,
30
+ DEFAULT_IMAGE_SIZES: () => DEFAULT_IMAGE_SIZES,
30
31
  HttpClient: () => HttpClient,
31
32
  NewsletterClient: () => NewsletterClient,
32
33
  OrganizationsClient: () => OrganizationsClient,
@@ -34,10 +35,15 @@ __export(index_exports, {
34
35
  ProductsClient: () => ProductsClient,
35
36
  SitesClient: () => SitesClient,
36
37
  WebhooksClient: () => WebhooksClient,
38
+ buildImageUrl: () => buildImageUrl,
37
39
  createApiError: () => createApiError,
38
40
  createCheckoutSession: () => createCheckoutSession,
39
41
  createPerspectApiClient: () => createPerspectApiClient,
40
42
  default: () => perspect_api_client_default,
43
+ generateResponsiveImageHtml: () => generateResponsiveImageHtml,
44
+ generateResponsiveUrls: () => generateResponsiveUrls,
45
+ generateSizesAttribute: () => generateSizesAttribute,
46
+ generateSrcSet: () => generateSrcSet,
41
47
  loadAllContent: () => loadAllContent,
42
48
  loadContentBySlug: () => loadContentBySlug,
43
49
  loadPages: () => loadPages,
@@ -45,6 +51,7 @@ __export(index_exports, {
45
51
  loadProductBySlug: () => loadProductBySlug,
46
52
  loadProducts: () => loadProducts,
47
53
  transformContent: () => transformContent,
54
+ transformMediaItem: () => transformMediaItem,
48
55
  transformProduct: () => transformProduct
49
56
  });
50
57
  module.exports = __toCommonJS(index_exports);
@@ -742,7 +749,10 @@ var ProductsClient = class extends BaseClient {
742
749
  delete normalizedParams.category_id;
743
750
  }
744
751
  }
745
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName, "/products")), normalizedParams);
752
+ return this.http.get(
753
+ this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
754
+ normalizedParams
755
+ );
746
756
  }
747
757
  /**
748
758
  * Get product by ID
@@ -761,7 +771,7 @@ var ProductsClient = class extends BaseClient {
761
771
  */
762
772
  async getProductBySlug(siteName, slug) {
763
773
  return this.http.get(this.buildPath(
764
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`)
774
+ this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
765
775
  ));
766
776
  }
767
777
  /**
@@ -859,7 +869,11 @@ var ProductsClient = class extends BaseClient {
859
869
  search: params.search
860
870
  } : void 0;
861
871
  return this.http.get(this.buildPath(
862
- this.siteScopedEndpoint(siteName, `/products/category/${encodeURIComponent(categorySlug)}`)
872
+ this.siteScopedEndpoint(
873
+ siteName,
874
+ `/products/category/${encodeURIComponent(categorySlug)}`,
875
+ { includeSitesSegment: false }
876
+ )
863
877
  ), queryParams);
864
878
  }
865
879
  };
@@ -1572,6 +1586,129 @@ function createPerspectApiClient(config) {
1572
1586
  }
1573
1587
  var perspect_api_client_default = PerspectApiClient;
1574
1588
 
1589
+ // src/utils/image-transform.ts
1590
+ var DEFAULT_IMAGE_SIZES = {
1591
+ thumbnail: {
1592
+ width: 150,
1593
+ height: 150,
1594
+ fit: "cover",
1595
+ quality: 85,
1596
+ format: "auto"
1597
+ },
1598
+ small: {
1599
+ width: 400,
1600
+ fit: "scale-down",
1601
+ quality: 85,
1602
+ format: "auto"
1603
+ },
1604
+ medium: {
1605
+ width: 800,
1606
+ fit: "scale-down",
1607
+ quality: 85,
1608
+ format: "auto"
1609
+ },
1610
+ large: {
1611
+ width: 1200,
1612
+ fit: "scale-down",
1613
+ quality: 85,
1614
+ format: "auto"
1615
+ },
1616
+ original: {
1617
+ format: "auto"
1618
+ }
1619
+ };
1620
+ function buildImageUrl(baseUrl, mediaPath, options) {
1621
+ const sourceUrl = `${baseUrl}/${mediaPath.replace(/^\//, "")}`;
1622
+ if (!options || Object.keys(options).length === 0) {
1623
+ return sourceUrl;
1624
+ }
1625
+ const params = [];
1626
+ if (options.width) params.push(`width=${options.width}`);
1627
+ if (options.height) params.push(`height=${options.height}`);
1628
+ if (options.fit) params.push(`fit=${options.fit}`);
1629
+ if (options.gravity) params.push(`gravity=${options.gravity}`);
1630
+ if (options.quality) params.push(`quality=${options.quality}`);
1631
+ if (options.format) params.push(`format=${options.format}`);
1632
+ if (options.sharpen) params.push(`sharpen=${options.sharpen}`);
1633
+ if (options.blur) params.push(`blur=${options.blur}`);
1634
+ if (options.rotate) params.push(`rotate=${options.rotate}`);
1635
+ if (options.dpr) params.push(`dpr=${options.dpr}`);
1636
+ if (options.metadata) params.push(`metadata=${options.metadata}`);
1637
+ if (options.background) params.push(`background=${options.background}`);
1638
+ if (options.trim) {
1639
+ const trimParts = [];
1640
+ if (options.trim.top) trimParts.push(`${options.trim.top}`);
1641
+ if (options.trim.right) trimParts.push(`${options.trim.right}`);
1642
+ if (options.trim.bottom) trimParts.push(`${options.trim.bottom}`);
1643
+ if (options.trim.left) trimParts.push(`${options.trim.left}`);
1644
+ if (trimParts.length > 0) {
1645
+ params.push(`trim=${trimParts.join(";")}`);
1646
+ }
1647
+ }
1648
+ const queryString = params.join(",");
1649
+ return `/cdn-cgi/image/${queryString}/${sourceUrl}`;
1650
+ }
1651
+ function generateResponsiveUrls(baseUrl, mediaPath, sizes = DEFAULT_IMAGE_SIZES) {
1652
+ return {
1653
+ thumbnail: buildImageUrl(baseUrl, mediaPath, sizes.thumbnail),
1654
+ small: buildImageUrl(baseUrl, mediaPath, sizes.small),
1655
+ medium: buildImageUrl(baseUrl, mediaPath, sizes.medium),
1656
+ large: buildImageUrl(baseUrl, mediaPath, sizes.large),
1657
+ original: buildImageUrl(baseUrl, mediaPath, sizes.original)
1658
+ };
1659
+ }
1660
+ function generateSrcSet(baseUrl, mediaPath, widths = [400, 800, 1200, 1600]) {
1661
+ return widths.map((width) => {
1662
+ const url = buildImageUrl(baseUrl, mediaPath, {
1663
+ width,
1664
+ fit: "scale-down",
1665
+ quality: 85,
1666
+ format: "auto"
1667
+ });
1668
+ return `${url} ${width}w`;
1669
+ }).join(", ");
1670
+ }
1671
+ function generateSizesAttribute(breakpoints = [
1672
+ { maxWidth: "640px", size: "100vw" },
1673
+ { maxWidth: "768px", size: "80vw" },
1674
+ { maxWidth: "1024px", size: "60vw" },
1675
+ { maxWidth: "1280px", size: "50vw" }
1676
+ ]) {
1677
+ const mediaQueries = breakpoints.map(
1678
+ (bp) => `(max-width: ${bp.maxWidth}) ${bp.size}`
1679
+ );
1680
+ mediaQueries.push("40vw");
1681
+ return mediaQueries.join(", ");
1682
+ }
1683
+ function generateResponsiveImageHtml(baseUrl, mediaPath, alt, options) {
1684
+ const srcset = generateSrcSet(baseUrl, mediaPath, options?.widths);
1685
+ const sizes = options?.sizes || generateSizesAttribute();
1686
+ const src = buildImageUrl(baseUrl, mediaPath, {
1687
+ width: 800,
1688
+ fit: "scale-down",
1689
+ quality: 85,
1690
+ format: "auto"
1691
+ });
1692
+ return `<img
1693
+ src="${src}"
1694
+ srcset="${srcset}"
1695
+ sizes="${sizes}"
1696
+ alt="${alt}"
1697
+ ${options?.className ? `class="${options.className}"` : ""}
1698
+ ${options?.loading ? `loading="${options.loading}"` : 'loading="lazy"'}
1699
+ ${options?.decoding ? `decoding="${options.decoding}"` : 'decoding="async"'}
1700
+ />`;
1701
+ }
1702
+ function transformMediaItem(baseUrl, media, sizes) {
1703
+ let mediaPath;
1704
+ if ("link" in media) {
1705
+ mediaPath = media.link.replace(/^https?:\/\/[^/]+\//, "");
1706
+ } else {
1707
+ mediaPath = `media/${media.site_name}/${media.r2_key.split("/").pop()}`;
1708
+ }
1709
+ return generateResponsiveUrls(baseUrl, mediaPath, sizes);
1710
+ }
1711
+
1575
1712
  // src/loaders.ts
1576
1713
  var noopLogger = {};
1577
1714
  var log = (logger, level, ...args) => {
@@ -1968,6 +2105,7 @@ async function createCheckoutSession(options) {
1968
2105
  CheckoutClient,
1969
2106
  ContactClient,
1970
2107
  ContentClient,
2108
+ DEFAULT_IMAGE_SIZES,
1971
2109
  HttpClient,
1972
2110
  NewsletterClient,
1973
2111
  OrganizationsClient,
@@ -1975,9 +2113,14 @@ async function createCheckoutSession(options) {
1975
2113
  ProductsClient,
1976
2114
  SitesClient,
1977
2115
  WebhooksClient,
2116
+ buildImageUrl,
1978
2117
  createApiError,
1979
2118
  createCheckoutSession,
1980
2119
  createPerspectApiClient,
2120
+ generateResponsiveImageHtml,
2121
+ generateResponsiveUrls,
2122
+ generateSizesAttribute,
2123
+ generateSrcSet,
1981
2124
  loadAllContent,
1982
2125
  loadContentBySlug,
1983
2126
  loadPages,
@@ -1985,5 +2128,6 @@ async function createCheckoutSession(options) {
1985
2128
  loadProductBySlug,
1986
2129
  loadProducts,
1987
2130
  transformContent,
2131
+ transformMediaItem,
1988
2132
  transformProduct
1989
2133
  });
package/dist/index.mjs CHANGED
@@ -691,7 +691,10 @@ var ProductsClient = class extends BaseClient {
691
691
  delete normalizedParams.category_id;
692
692
  }
693
693
  }
694
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName, "/products")), normalizedParams);
694
+ return this.http.get(
695
+ this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
696
+ normalizedParams
697
+ );
695
698
  }
696
699
  /**
697
700
  * Get product by ID
@@ -710,7 +713,7 @@ var ProductsClient = class extends BaseClient {
710
713
  */
711
714
  async getProductBySlug(siteName, slug) {
712
715
  return this.http.get(this.buildPath(
713
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`)
716
+ this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
714
717
  ));
715
718
  }
716
719
  /**
@@ -808,7 +811,11 @@ var ProductsClient = class extends BaseClient {
808
811
  search: params.search
809
812
  } : void 0;
810
813
  return this.http.get(this.buildPath(
811
- this.siteScopedEndpoint(siteName, `/products/category/${encodeURIComponent(categorySlug)}`)
814
+ this.siteScopedEndpoint(
815
+ siteName,
816
+ `/products/category/${encodeURIComponent(categorySlug)}`,
817
+ { includeSitesSegment: false }
818
+ )
812
819
  ), queryParams);
813
820
  }
814
821
  };
@@ -1521,6 +1528,129 @@ function createPerspectApiClient(config) {
1521
1528
  }
1522
1529
  var perspect_api_client_default = PerspectApiClient;
1523
1530
 
1531
+ // src/utils/image-transform.ts
1532
+ var DEFAULT_IMAGE_SIZES = {
1533
+ thumbnail: {
1534
+ width: 150,
1535
+ height: 150,
1536
+ fit: "cover",
1537
+ quality: 85,
1538
+ format: "auto"
1539
+ },
1540
+ small: {
1541
+ width: 400,
1542
+ fit: "scale-down",
1543
+ quality: 85,
1544
+ format: "auto"
1545
+ },
1546
+ medium: {
1547
+ width: 800,
1548
+ fit: "scale-down",
1549
+ quality: 85,
1550
+ format: "auto"
1551
+ },
1552
+ large: {
1553
+ width: 1200,
1554
+ fit: "scale-down",
1555
+ quality: 85,
1556
+ format: "auto"
1557
+ },
1558
+ original: {
1559
+ format: "auto"
1560
+ }
1561
+ };
1562
+ function buildImageUrl(baseUrl, mediaPath, options) {
1563
+ const sourceUrl = `${baseUrl}/${mediaPath.replace(/^\//, "")}`;
1564
+ if (!options || Object.keys(options).length === 0) {
1565
+ return sourceUrl;
1566
+ }
1567
+ const params = [];
1568
+ if (options.width) params.push(`width=${options.width}`);
1569
+ if (options.height) params.push(`height=${options.height}`);
1570
+ if (options.fit) params.push(`fit=${options.fit}`);
1571
+ if (options.gravity) params.push(`gravity=${options.gravity}`);
1572
+ if (options.quality) params.push(`quality=${options.quality}`);
1573
+ if (options.format) params.push(`format=${options.format}`);
1574
+ if (options.sharpen) params.push(`sharpen=${options.sharpen}`);
1575
+ if (options.blur) params.push(`blur=${options.blur}`);
1576
+ if (options.rotate) params.push(`rotate=${options.rotate}`);
1577
+ if (options.dpr) params.push(`dpr=${options.dpr}`);
1578
+ if (options.metadata) params.push(`metadata=${options.metadata}`);
1579
+ if (options.background) params.push(`background=${options.background}`);
1580
+ if (options.trim) {
1581
+ const trimParts = [];
1582
+ if (options.trim.top) trimParts.push(`${options.trim.top}`);
1583
+ if (options.trim.right) trimParts.push(`${options.trim.right}`);
1584
+ if (options.trim.bottom) trimParts.push(`${options.trim.bottom}`);
1585
+ if (options.trim.left) trimParts.push(`${options.trim.left}`);
1586
+ if (trimParts.length > 0) {
1587
+ params.push(`trim=${trimParts.join(";")}`);
1588
+ }
1589
+ }
1590
+ const queryString = params.join(",");
1591
+ return `/cdn-cgi/image/${queryString}/${sourceUrl}`;
1592
+ }
1593
+ function generateResponsiveUrls(baseUrl, mediaPath, sizes = DEFAULT_IMAGE_SIZES) {
1594
+ return {
1595
+ thumbnail: buildImageUrl(baseUrl, mediaPath, sizes.thumbnail),
1596
+ small: buildImageUrl(baseUrl, mediaPath, sizes.small),
1597
+ medium: buildImageUrl(baseUrl, mediaPath, sizes.medium),
1598
+ large: buildImageUrl(baseUrl, mediaPath, sizes.large),
1599
+ original: buildImageUrl(baseUrl, mediaPath, sizes.original)
1600
+ };
1601
+ }
1602
+ function generateSrcSet(baseUrl, mediaPath, widths = [400, 800, 1200, 1600]) {
1603
+ return widths.map((width) => {
1604
+ const url = buildImageUrl(baseUrl, mediaPath, {
1605
+ width,
1606
+ fit: "scale-down",
1607
+ quality: 85,
1608
+ format: "auto"
1609
+ });
1610
+ return `${url} ${width}w`;
1611
+ }).join(", ");
1612
+ }
1613
+ function generateSizesAttribute(breakpoints = [
1614
+ { maxWidth: "640px", size: "100vw" },
1615
+ { maxWidth: "768px", size: "80vw" },
1616
+ { maxWidth: "1024px", size: "60vw" },
1617
+ { maxWidth: "1280px", size: "50vw" }
1618
+ ]) {
1619
+ const mediaQueries = breakpoints.map(
1620
+ (bp) => `(max-width: ${bp.maxWidth}) ${bp.size}`
1621
+ );
1622
+ mediaQueries.push("40vw");
1623
+ return mediaQueries.join(", ");
1624
+ }
1625
+ function generateResponsiveImageHtml(baseUrl, mediaPath, alt, options) {
1626
+ const srcset = generateSrcSet(baseUrl, mediaPath, options?.widths);
1627
+ const sizes = options?.sizes || generateSizesAttribute();
1628
+ const src = buildImageUrl(baseUrl, mediaPath, {
1629
+ width: 800,
1630
+ fit: "scale-down",
1631
+ quality: 85,
1632
+ format: "auto"
1633
+ });
1634
+ return `<img
1635
+ src="${src}"
1636
+ srcset="${srcset}"
1637
+ sizes="${sizes}"
1638
+ alt="${alt}"
1639
+ ${options?.className ? `class="${options.className}"` : ""}
1640
+ ${options?.loading ? `loading="${options.loading}"` : 'loading="lazy"'}
1641
+ ${options?.decoding ? `decoding="${options.decoding}"` : 'decoding="async"'}
1642
+ />`;
1643
+ }
1644
+ function transformMediaItem(baseUrl, media, sizes) {
1645
+ let mediaPath;
1646
+ if ("link" in media) {
1647
+ mediaPath = media.link.replace(/^https?:\/\/[^/]+\//, "");
1648
+ } else {
1649
+ mediaPath = `media/${media.site_name}/${media.r2_key.split("/").pop()}`;
1650
+ }
1651
+ return generateResponsiveUrls(baseUrl, mediaPath, sizes);
1652
+ }
1653
+
1524
1654
  // src/loaders.ts
1525
1655
  var noopLogger = {};
1526
1656
  var log = (logger, level, ...args) => {
@@ -1916,6 +2046,7 @@ export {
1916
2046
  CheckoutClient,
1917
2047
  ContactClient,
1918
2048
  ContentClient,
2049
+ DEFAULT_IMAGE_SIZES,
1919
2050
  HttpClient,
1920
2051
  NewsletterClient,
1921
2052
  OrganizationsClient,
@@ -1923,10 +2054,15 @@ export {
1923
2054
  ProductsClient,
1924
2055
  SitesClient,
1925
2056
  WebhooksClient,
2057
+ buildImageUrl,
1926
2058
  createApiError,
1927
2059
  createCheckoutSession,
1928
2060
  createPerspectApiClient,
1929
2061
  perspect_api_client_default as default,
2062
+ generateResponsiveImageHtml,
2063
+ generateResponsiveUrls,
2064
+ generateSizesAttribute,
2065
+ generateSrcSet,
1930
2066
  loadAllContent,
1931
2067
  loadContentBySlug,
1932
2068
  loadPages,
@@ -1934,5 +2070,6 @@ export {
1934
2070
  loadProductBySlug,
1935
2071
  loadProducts,
1936
2072
  transformContent,
2073
+ transformMediaItem,
1937
2074
  transformProduct
1938
2075
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perspectapi-ts-sdk",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -50,7 +50,10 @@ export class ProductsClient extends BaseClient {
50
50
  }
51
51
  }
52
52
 
53
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName, '/products')), normalizedParams);
53
+ return this.http.get(
54
+ this.buildPath(this.siteScopedEndpoint(siteName, '/products', { includeSitesSegment: false })),
55
+ normalizedParams
56
+ );
54
57
  }
55
58
 
56
59
  /**
@@ -72,7 +75,7 @@ export class ProductsClient extends BaseClient {
72
75
  */
73
76
  async getProductBySlug(siteName: string, slug: string): Promise<ApiResponse<Product & { variants?: any[] }>> {
74
77
  return this.http.get(this.buildPath(
75
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`)
78
+ this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
76
79
  ));
77
80
  }
78
81
 
@@ -241,7 +244,11 @@ export class ProductsClient extends BaseClient {
241
244
  } : undefined;
242
245
 
243
246
  return this.http.get(this.buildPath(
244
- this.siteScopedEndpoint(siteName, `/products/category/${encodeURIComponent(categorySlug)}`)
247
+ this.siteScopedEndpoint(
248
+ siteName,
249
+ `/products/category/${encodeURIComponent(categorySlug)}`,
250
+ { includeSitesSegment: false }
251
+ )
245
252
  ), queryParams);
246
253
  }
247
254
  }
package/src/index.ts CHANGED
@@ -26,6 +26,22 @@ export { BaseClient } from './client/base-client';
26
26
  // Utilities
27
27
  export { HttpClient, createApiError } from './utils/http-client';
28
28
 
29
+ // Image transformation utilities
30
+ export {
31
+ buildImageUrl,
32
+ generateResponsiveUrls,
33
+ generateSrcSet,
34
+ generateSizesAttribute,
35
+ generateResponsiveImageHtml,
36
+ transformMediaItem,
37
+ DEFAULT_IMAGE_SIZES
38
+ } from './utils/image-transform';
39
+
40
+ export type {
41
+ ImageTransformOptions,
42
+ ResponsiveImageSizes
43
+ } from './utils/image-transform';
44
+
29
45
  // High-level data loaders
30
46
  export {
31
47
  loadProducts,
@@ -221,6 +221,7 @@ export interface Content {
221
221
  contentMarkdown?: string;
222
222
  custom?: Record<string, any>;
223
223
  slug?: string;
224
+ slug_prefix?: string;
224
225
  pageStatus: ContentStatus;
225
226
  pageType: ContentType;
226
227
  userId?: number;
@@ -246,6 +247,7 @@ export interface ContentQueryParams extends PaginationParams {
246
247
  page_type?: ContentType;
247
248
  search?: string;
248
249
  user_id?: number;
250
+ slug_prefix?: string;
249
251
  }
250
252
 
251
253
  // Categories
@@ -320,6 +322,7 @@ export interface ProductQueryParams extends PaginationParams {
320
322
  search?: string;
321
323
  category?: string | string[];
322
324
  category_id?: number | string | Array<number | string>;
325
+ slug_prefix?: string;
323
326
  }
324
327
 
325
328
  export interface BlogPost {
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Cloudflare Image Resizing Integration
3
+ * Transforms images on-the-fly using Cloudflare's Image Resizing service
4
+ * https://developers.cloudflare.com/images/transform-images/transform-via-url/
5
+ */
6
+
7
+ export interface ImageTransformOptions {
8
+ width?: number;
9
+ height?: number;
10
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
11
+ gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center';
12
+ quality?: number; // 1-100
13
+ format?: 'auto' | 'avif' | 'webp' | 'json' | 'jpeg' | 'png';
14
+ sharpen?: number; // 0-10
15
+ blur?: number; // 0-250
16
+ rotate?: 0 | 90 | 180 | 270;
17
+ dpr?: number; // Device Pixel Ratio: 1, 2, or 3
18
+ metadata?: 'keep' | 'copyright' | 'none';
19
+ background?: string; // hex color for padding
20
+ trim?: {
21
+ top?: number;
22
+ right?: number;
23
+ bottom?: number;
24
+ left?: number;
25
+ };
26
+ }
27
+
28
+ export interface ResponsiveImageSizes {
29
+ thumbnail: ImageTransformOptions;
30
+ small: ImageTransformOptions;
31
+ medium: ImageTransformOptions;
32
+ large: ImageTransformOptions;
33
+ original: ImageTransformOptions;
34
+ }
35
+
36
+ /**
37
+ * Default responsive image sizes
38
+ */
39
+ export const DEFAULT_IMAGE_SIZES: ResponsiveImageSizes = {
40
+ thumbnail: {
41
+ width: 150,
42
+ height: 150,
43
+ fit: 'cover',
44
+ quality: 85,
45
+ format: 'auto'
46
+ },
47
+ small: {
48
+ width: 400,
49
+ fit: 'scale-down',
50
+ quality: 85,
51
+ format: 'auto'
52
+ },
53
+ medium: {
54
+ width: 800,
55
+ fit: 'scale-down',
56
+ quality: 85,
57
+ format: 'auto'
58
+ },
59
+ large: {
60
+ width: 1200,
61
+ fit: 'scale-down',
62
+ quality: 85,
63
+ format: 'auto'
64
+ },
65
+ original: {
66
+ format: 'auto'
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Build Cloudflare Image Resizing URL
72
+ *
73
+ * @param baseUrl - The base URL of your API (e.g., "https://api.perspect.co")
74
+ * @param mediaPath - The path to the media file (e.g., "media/site/image.jpg")
75
+ * @param options - Transform options
76
+ * @returns Cloudflare Image Resizing URL
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const url = buildImageUrl(
81
+ * 'https://api.perspect.co',
82
+ * 'media/mysite/photo.jpg',
83
+ * { width: 400, format: 'webp', quality: 85 }
84
+ * );
85
+ * // Returns: '/cdn-cgi/image/width=400,format=webp,quality=85/https://api.perspect.co/media/mysite/photo.jpg'
86
+ * ```
87
+ */
88
+ export function buildImageUrl(
89
+ baseUrl: string,
90
+ mediaPath: string,
91
+ options?: ImageTransformOptions
92
+ ): string {
93
+ // Construct the full source URL
94
+ const sourceUrl = `${baseUrl}/${mediaPath.replace(/^\//, '')}`;
95
+
96
+ if (!options || Object.keys(options).length === 0) {
97
+ // Return original image URL
98
+ return sourceUrl;
99
+ }
100
+
101
+ // Build transform options string
102
+ const params: string[] = [];
103
+
104
+ if (options.width) params.push(`width=${options.width}`);
105
+ if (options.height) params.push(`height=${options.height}`);
106
+ if (options.fit) params.push(`fit=${options.fit}`);
107
+ if (options.gravity) params.push(`gravity=${options.gravity}`);
108
+ if (options.quality) params.push(`quality=${options.quality}`);
109
+ if (options.format) params.push(`format=${options.format}`);
110
+ if (options.sharpen) params.push(`sharpen=${options.sharpen}`);
111
+ if (options.blur) params.push(`blur=${options.blur}`);
112
+ if (options.rotate) params.push(`rotate=${options.rotate}`);
113
+ if (options.dpr) params.push(`dpr=${options.dpr}`);
114
+ if (options.metadata) params.push(`metadata=${options.metadata}`);
115
+ if (options.background) params.push(`background=${options.background}`);
116
+
117
+ if (options.trim) {
118
+ const trimParts: string[] = [];
119
+ if (options.trim.top) trimParts.push(`${options.trim.top}`);
120
+ if (options.trim.right) trimParts.push(`${options.trim.right}`);
121
+ if (options.trim.bottom) trimParts.push(`${options.trim.bottom}`);
122
+ if (options.trim.left) trimParts.push(`${options.trim.left}`);
123
+ if (trimParts.length > 0) {
124
+ params.push(`trim=${trimParts.join(';')}`);
125
+ }
126
+ }
127
+
128
+ const queryString = params.join(',');
129
+
130
+ // Cloudflare Image Resizing URL format:
131
+ // /cdn-cgi/image/{options}/{source-image-url}
132
+ // Note: This only works on Cloudflare zones with Image Resizing enabled
133
+ return `/cdn-cgi/image/${queryString}/${sourceUrl}`;
134
+ }
135
+
136
+ /**
137
+ * Generate responsive image URLs for different sizes
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const urls = generateResponsiveUrls(
142
+ * 'https://api.perspect.co',
143
+ * 'media/mysite/photo.jpg'
144
+ * );
145
+ * // Returns: { thumbnail: '...', small: '...', medium: '...', large: '...', original: '...' }
146
+ * ```
147
+ */
148
+ export function generateResponsiveUrls(
149
+ baseUrl: string,
150
+ mediaPath: string,
151
+ sizes: ResponsiveImageSizes = DEFAULT_IMAGE_SIZES
152
+ ): Record<keyof ResponsiveImageSizes, string> {
153
+ return {
154
+ thumbnail: buildImageUrl(baseUrl, mediaPath, sizes.thumbnail),
155
+ small: buildImageUrl(baseUrl, mediaPath, sizes.small),
156
+ medium: buildImageUrl(baseUrl, mediaPath, sizes.medium),
157
+ large: buildImageUrl(baseUrl, mediaPath, sizes.large),
158
+ original: buildImageUrl(baseUrl, mediaPath, sizes.original)
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Generate srcset for responsive images
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const srcset = generateSrcSet(
168
+ * 'https://api.perspect.co',
169
+ * 'media/mysite/photo.jpg',
170
+ * [400, 800, 1200]
171
+ * );
172
+ * // Returns: "...?width=400 400w, ...?width=800 800w, ...?width=1200 1200w"
173
+ * ```
174
+ */
175
+ export function generateSrcSet(
176
+ baseUrl: string,
177
+ mediaPath: string,
178
+ widths: number[] = [400, 800, 1200, 1600]
179
+ ): string {
180
+ return widths
181
+ .map((width) => {
182
+ const url = buildImageUrl(baseUrl, mediaPath, {
183
+ width,
184
+ fit: 'scale-down',
185
+ quality: 85,
186
+ format: 'auto'
187
+ });
188
+ return `${url} ${width}w`;
189
+ })
190
+ .join(', ');
191
+ }
192
+
193
+ /**
194
+ * Generate sizes attribute for responsive images
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const sizes = generateSizesAttribute();
199
+ * // Returns: "(max-width: 640px) 100vw, (max-width: 768px) 80vw, ..."
200
+ * ```
201
+ */
202
+ export function generateSizesAttribute(
203
+ breakpoints: Array<{ maxWidth: string; size: string }> = [
204
+ { maxWidth: '640px', size: '100vw' },
205
+ { maxWidth: '768px', size: '80vw' },
206
+ { maxWidth: '1024px', size: '60vw' },
207
+ { maxWidth: '1280px', size: '50vw' }
208
+ ]
209
+ ): string {
210
+ const mediaQueries = breakpoints.map(
211
+ (bp) => `(max-width: ${bp.maxWidth}) ${bp.size}`
212
+ );
213
+ mediaQueries.push('40vw'); // default size
214
+ return mediaQueries.join(', ');
215
+ }
216
+
217
+ /**
218
+ * Helper to generate complete responsive image HTML
219
+ *
220
+ * @example
221
+ * ```typescript
222
+ * const html = generateResponsiveImageHtml(
223
+ * 'https://api.perspect.co',
224
+ * 'media/mysite/photo.jpg',
225
+ * 'My photo',
226
+ * { className: 'rounded-lg', loading: 'lazy' }
227
+ * );
228
+ * ```
229
+ */
230
+ export function generateResponsiveImageHtml(
231
+ baseUrl: string,
232
+ mediaPath: string,
233
+ alt: string,
234
+ options?: {
235
+ className?: string;
236
+ loading?: 'lazy' | 'eager';
237
+ decoding?: 'async' | 'sync' | 'auto';
238
+ sizes?: string;
239
+ widths?: number[];
240
+ }
241
+ ): string {
242
+ const srcset = generateSrcSet(baseUrl, mediaPath, options?.widths);
243
+ const sizes = options?.sizes || generateSizesAttribute();
244
+ const src = buildImageUrl(baseUrl, mediaPath, {
245
+ width: 800,
246
+ fit: 'scale-down',
247
+ quality: 85,
248
+ format: 'auto'
249
+ });
250
+
251
+ return `<img
252
+ src="${src}"
253
+ srcset="${srcset}"
254
+ sizes="${sizes}"
255
+ alt="${alt}"
256
+ ${options?.className ? `class="${options.className}"` : ''}
257
+ ${options?.loading ? `loading="${options.loading}"` : 'loading="lazy"'}
258
+ ${options?.decoding ? `decoding="${options.decoding}"` : 'decoding="async"'}
259
+ />`;
260
+ }
261
+
262
+ /**
263
+ * Transform a MediaItem into responsive URLs
264
+ * Convenience function for working with MediaItem objects from the API
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * import { transformMediaItem } from 'perspectapi-ts-sdk';
269
+ *
270
+ * const product = await client.products.getProduct('mysite', 123);
271
+ * const media = product.data.media?.[0];
272
+ *
273
+ * if (media) {
274
+ * const urls = transformMediaItem('https://api.perspect.co', media);
275
+ * console.log(urls.thumbnail); // Cloudflare-transformed thumbnail URL
276
+ * }
277
+ * ```
278
+ */
279
+ export function transformMediaItem(
280
+ baseUrl: string,
281
+ media: { link: string } | { r2_key: string; site_name: string },
282
+ sizes?: ResponsiveImageSizes
283
+ ): Record<keyof ResponsiveImageSizes, string> {
284
+ // Determine the media path
285
+ let mediaPath: string;
286
+
287
+ if ('link' in media) {
288
+ // Extract path from full URL
289
+ mediaPath = media.link.replace(/^https?:\/\/[^/]+\//, '');
290
+ } else {
291
+ // Construct from r2_key
292
+ mediaPath = `media/${media.site_name}/${media.r2_key.split('/').pop()}`;
293
+ }
294
+
295
+ return generateResponsiveUrls(baseUrl, mediaPath, sizes);
296
+ }