perspectapi-ts-sdk 1.4.1 → 1.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/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:
package/dist/index.d.mts CHANGED
@@ -1925,6 +1925,145 @@ declare class PerspectApiClient {
1925
1925
  */
1926
1926
  declare function createPerspectApiClient(config: PerspectApiConfig): PerspectApiClient;
1927
1927
 
1928
+ /**
1929
+ * Cloudflare Image Resizing Integration
1930
+ * Transforms images on-the-fly using Cloudflare's Image Resizing service
1931
+ * https://developers.cloudflare.com/images/transform-images/transform-via-url/
1932
+ */
1933
+ interface ImageTransformOptions {
1934
+ width?: number;
1935
+ height?: number;
1936
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
1937
+ gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center';
1938
+ quality?: number;
1939
+ format?: 'auto' | 'avif' | 'webp' | 'json' | 'jpeg' | 'png';
1940
+ sharpen?: number;
1941
+ blur?: number;
1942
+ rotate?: 0 | 90 | 180 | 270;
1943
+ dpr?: number;
1944
+ metadata?: 'keep' | 'copyright' | 'none';
1945
+ background?: string;
1946
+ trim?: {
1947
+ top?: number;
1948
+ right?: number;
1949
+ bottom?: number;
1950
+ left?: number;
1951
+ };
1952
+ }
1953
+ interface ResponsiveImageSizes {
1954
+ thumbnail: ImageTransformOptions;
1955
+ small: ImageTransformOptions;
1956
+ medium: ImageTransformOptions;
1957
+ large: ImageTransformOptions;
1958
+ original: ImageTransformOptions;
1959
+ }
1960
+ /**
1961
+ * Default responsive image sizes
1962
+ */
1963
+ declare const DEFAULT_IMAGE_SIZES: ResponsiveImageSizes;
1964
+ /**
1965
+ * Build Cloudflare Image Resizing URL
1966
+ *
1967
+ * @param baseUrl - The base URL of your API (e.g., "https://api.perspect.co")
1968
+ * @param mediaPath - The path to the media file (e.g., "media/site/image.jpg")
1969
+ * @param options - Transform options
1970
+ * @returns Cloudflare Image Resizing URL
1971
+ *
1972
+ * @example
1973
+ * ```typescript
1974
+ * const url = buildImageUrl(
1975
+ * 'https://api.perspect.co',
1976
+ * 'media/mysite/photo.jpg',
1977
+ * { width: 400, format: 'webp', quality: 85 }
1978
+ * );
1979
+ * // Returns: '/cdn-cgi/image/width=400,format=webp,quality=85/https://api.perspect.co/media/mysite/photo.jpg'
1980
+ * ```
1981
+ */
1982
+ declare function buildImageUrl(baseUrl: string, mediaPath: string, options?: ImageTransformOptions): string;
1983
+ /**
1984
+ * Generate responsive image URLs for different sizes
1985
+ *
1986
+ * @example
1987
+ * ```typescript
1988
+ * const urls = generateResponsiveUrls(
1989
+ * 'https://api.perspect.co',
1990
+ * 'media/mysite/photo.jpg'
1991
+ * );
1992
+ * // Returns: { thumbnail: '...', small: '...', medium: '...', large: '...', original: '...' }
1993
+ * ```
1994
+ */
1995
+ declare function generateResponsiveUrls(baseUrl: string, mediaPath: string, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
1996
+ /**
1997
+ * Generate srcset for responsive images
1998
+ *
1999
+ * @example
2000
+ * ```typescript
2001
+ * const srcset = generateSrcSet(
2002
+ * 'https://api.perspect.co',
2003
+ * 'media/mysite/photo.jpg',
2004
+ * [400, 800, 1200]
2005
+ * );
2006
+ * // Returns: "...?width=400 400w, ...?width=800 800w, ...?width=1200 1200w"
2007
+ * ```
2008
+ */
2009
+ declare function generateSrcSet(baseUrl: string, mediaPath: string, widths?: number[]): string;
2010
+ /**
2011
+ * Generate sizes attribute for responsive images
2012
+ *
2013
+ * @example
2014
+ * ```typescript
2015
+ * const sizes = generateSizesAttribute();
2016
+ * // Returns: "(max-width: 640px) 100vw, (max-width: 768px) 80vw, ..."
2017
+ * ```
2018
+ */
2019
+ declare function generateSizesAttribute(breakpoints?: Array<{
2020
+ maxWidth: string;
2021
+ size: string;
2022
+ }>): string;
2023
+ /**
2024
+ * Helper to generate complete responsive image HTML
2025
+ *
2026
+ * @example
2027
+ * ```typescript
2028
+ * const html = generateResponsiveImageHtml(
2029
+ * 'https://api.perspect.co',
2030
+ * 'media/mysite/photo.jpg',
2031
+ * 'My photo',
2032
+ * { className: 'rounded-lg', loading: 'lazy' }
2033
+ * );
2034
+ * ```
2035
+ */
2036
+ declare function generateResponsiveImageHtml(baseUrl: string, mediaPath: string, alt: string, options?: {
2037
+ className?: string;
2038
+ loading?: 'lazy' | 'eager';
2039
+ decoding?: 'async' | 'sync' | 'auto';
2040
+ sizes?: string;
2041
+ widths?: number[];
2042
+ }): string;
2043
+ /**
2044
+ * Transform a MediaItem into responsive URLs
2045
+ * Convenience function for working with MediaItem objects from the API
2046
+ *
2047
+ * @example
2048
+ * ```typescript
2049
+ * import { transformMediaItem } from 'perspectapi-ts-sdk';
2050
+ *
2051
+ * const product = await client.products.getProduct('mysite', 123);
2052
+ * const media = product.data.media?.[0];
2053
+ *
2054
+ * if (media) {
2055
+ * const urls = transformMediaItem('https://api.perspect.co', media);
2056
+ * console.log(urls.thumbnail); // Cloudflare-transformed thumbnail URL
2057
+ * }
2058
+ * ```
2059
+ */
2060
+ declare function transformMediaItem(baseUrl: string, media: {
2061
+ link: string;
2062
+ } | {
2063
+ r2_key: string;
2064
+ site_name: string;
2065
+ }, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
2066
+
1928
2067
  /**
1929
2068
  * High-level data loading helpers that wrap the PerspectAPI SDK clients.
1930
2069
  * These helpers provide convenient product and content loading utilities with
@@ -2057,4 +2196,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2057
2196
  error: string;
2058
2197
  }>;
2059
2198
 
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 };
2199
+ 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
@@ -1925,6 +1925,145 @@ declare class PerspectApiClient {
1925
1925
  */
1926
1926
  declare function createPerspectApiClient(config: PerspectApiConfig): PerspectApiClient;
1927
1927
 
1928
+ /**
1929
+ * Cloudflare Image Resizing Integration
1930
+ * Transforms images on-the-fly using Cloudflare's Image Resizing service
1931
+ * https://developers.cloudflare.com/images/transform-images/transform-via-url/
1932
+ */
1933
+ interface ImageTransformOptions {
1934
+ width?: number;
1935
+ height?: number;
1936
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
1937
+ gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center';
1938
+ quality?: number;
1939
+ format?: 'auto' | 'avif' | 'webp' | 'json' | 'jpeg' | 'png';
1940
+ sharpen?: number;
1941
+ blur?: number;
1942
+ rotate?: 0 | 90 | 180 | 270;
1943
+ dpr?: number;
1944
+ metadata?: 'keep' | 'copyright' | 'none';
1945
+ background?: string;
1946
+ trim?: {
1947
+ top?: number;
1948
+ right?: number;
1949
+ bottom?: number;
1950
+ left?: number;
1951
+ };
1952
+ }
1953
+ interface ResponsiveImageSizes {
1954
+ thumbnail: ImageTransformOptions;
1955
+ small: ImageTransformOptions;
1956
+ medium: ImageTransformOptions;
1957
+ large: ImageTransformOptions;
1958
+ original: ImageTransformOptions;
1959
+ }
1960
+ /**
1961
+ * Default responsive image sizes
1962
+ */
1963
+ declare const DEFAULT_IMAGE_SIZES: ResponsiveImageSizes;
1964
+ /**
1965
+ * Build Cloudflare Image Resizing URL
1966
+ *
1967
+ * @param baseUrl - The base URL of your API (e.g., "https://api.perspect.co")
1968
+ * @param mediaPath - The path to the media file (e.g., "media/site/image.jpg")
1969
+ * @param options - Transform options
1970
+ * @returns Cloudflare Image Resizing URL
1971
+ *
1972
+ * @example
1973
+ * ```typescript
1974
+ * const url = buildImageUrl(
1975
+ * 'https://api.perspect.co',
1976
+ * 'media/mysite/photo.jpg',
1977
+ * { width: 400, format: 'webp', quality: 85 }
1978
+ * );
1979
+ * // Returns: '/cdn-cgi/image/width=400,format=webp,quality=85/https://api.perspect.co/media/mysite/photo.jpg'
1980
+ * ```
1981
+ */
1982
+ declare function buildImageUrl(baseUrl: string, mediaPath: string, options?: ImageTransformOptions): string;
1983
+ /**
1984
+ * Generate responsive image URLs for different sizes
1985
+ *
1986
+ * @example
1987
+ * ```typescript
1988
+ * const urls = generateResponsiveUrls(
1989
+ * 'https://api.perspect.co',
1990
+ * 'media/mysite/photo.jpg'
1991
+ * );
1992
+ * // Returns: { thumbnail: '...', small: '...', medium: '...', large: '...', original: '...' }
1993
+ * ```
1994
+ */
1995
+ declare function generateResponsiveUrls(baseUrl: string, mediaPath: string, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
1996
+ /**
1997
+ * Generate srcset for responsive images
1998
+ *
1999
+ * @example
2000
+ * ```typescript
2001
+ * const srcset = generateSrcSet(
2002
+ * 'https://api.perspect.co',
2003
+ * 'media/mysite/photo.jpg',
2004
+ * [400, 800, 1200]
2005
+ * );
2006
+ * // Returns: "...?width=400 400w, ...?width=800 800w, ...?width=1200 1200w"
2007
+ * ```
2008
+ */
2009
+ declare function generateSrcSet(baseUrl: string, mediaPath: string, widths?: number[]): string;
2010
+ /**
2011
+ * Generate sizes attribute for responsive images
2012
+ *
2013
+ * @example
2014
+ * ```typescript
2015
+ * const sizes = generateSizesAttribute();
2016
+ * // Returns: "(max-width: 640px) 100vw, (max-width: 768px) 80vw, ..."
2017
+ * ```
2018
+ */
2019
+ declare function generateSizesAttribute(breakpoints?: Array<{
2020
+ maxWidth: string;
2021
+ size: string;
2022
+ }>): string;
2023
+ /**
2024
+ * Helper to generate complete responsive image HTML
2025
+ *
2026
+ * @example
2027
+ * ```typescript
2028
+ * const html = generateResponsiveImageHtml(
2029
+ * 'https://api.perspect.co',
2030
+ * 'media/mysite/photo.jpg',
2031
+ * 'My photo',
2032
+ * { className: 'rounded-lg', loading: 'lazy' }
2033
+ * );
2034
+ * ```
2035
+ */
2036
+ declare function generateResponsiveImageHtml(baseUrl: string, mediaPath: string, alt: string, options?: {
2037
+ className?: string;
2038
+ loading?: 'lazy' | 'eager';
2039
+ decoding?: 'async' | 'sync' | 'auto';
2040
+ sizes?: string;
2041
+ widths?: number[];
2042
+ }): string;
2043
+ /**
2044
+ * Transform a MediaItem into responsive URLs
2045
+ * Convenience function for working with MediaItem objects from the API
2046
+ *
2047
+ * @example
2048
+ * ```typescript
2049
+ * import { transformMediaItem } from 'perspectapi-ts-sdk';
2050
+ *
2051
+ * const product = await client.products.getProduct('mysite', 123);
2052
+ * const media = product.data.media?.[0];
2053
+ *
2054
+ * if (media) {
2055
+ * const urls = transformMediaItem('https://api.perspect.co', media);
2056
+ * console.log(urls.thumbnail); // Cloudflare-transformed thumbnail URL
2057
+ * }
2058
+ * ```
2059
+ */
2060
+ declare function transformMediaItem(baseUrl: string, media: {
2061
+ link: string;
2062
+ } | {
2063
+ r2_key: string;
2064
+ site_name: string;
2065
+ }, sizes?: ResponsiveImageSizes): Record<keyof ResponsiveImageSizes, string>;
2066
+
1928
2067
  /**
1929
2068
  * High-level data loading helpers that wrap the PerspectAPI SDK clients.
1930
2069
  * These helpers provide convenient product and content loading utilities with
@@ -2057,4 +2196,4 @@ declare function createCheckoutSession(options: CheckoutSessionOptions): Promise
2057
2196
  error: string;
2058
2197
  }>;
2059
2198
 
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 };
2199
+ 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);
@@ -1572,6 +1579,129 @@ function createPerspectApiClient(config) {
1572
1579
  }
1573
1580
  var perspect_api_client_default = PerspectApiClient;
1574
1581
 
1582
+ // src/utils/image-transform.ts
1583
+ var DEFAULT_IMAGE_SIZES = {
1584
+ thumbnail: {
1585
+ width: 150,
1586
+ height: 150,
1587
+ fit: "cover",
1588
+ quality: 85,
1589
+ format: "auto"
1590
+ },
1591
+ small: {
1592
+ width: 400,
1593
+ fit: "scale-down",
1594
+ quality: 85,
1595
+ format: "auto"
1596
+ },
1597
+ medium: {
1598
+ width: 800,
1599
+ fit: "scale-down",
1600
+ quality: 85,
1601
+ format: "auto"
1602
+ },
1603
+ large: {
1604
+ width: 1200,
1605
+ fit: "scale-down",
1606
+ quality: 85,
1607
+ format: "auto"
1608
+ },
1609
+ original: {
1610
+ format: "auto"
1611
+ }
1612
+ };
1613
+ function buildImageUrl(baseUrl, mediaPath, options) {
1614
+ const sourceUrl = `${baseUrl}/${mediaPath.replace(/^\//, "")}`;
1615
+ if (!options || Object.keys(options).length === 0) {
1616
+ return sourceUrl;
1617
+ }
1618
+ const params = [];
1619
+ if (options.width) params.push(`width=${options.width}`);
1620
+ if (options.height) params.push(`height=${options.height}`);
1621
+ if (options.fit) params.push(`fit=${options.fit}`);
1622
+ if (options.gravity) params.push(`gravity=${options.gravity}`);
1623
+ if (options.quality) params.push(`quality=${options.quality}`);
1624
+ if (options.format) params.push(`format=${options.format}`);
1625
+ if (options.sharpen) params.push(`sharpen=${options.sharpen}`);
1626
+ if (options.blur) params.push(`blur=${options.blur}`);
1627
+ if (options.rotate) params.push(`rotate=${options.rotate}`);
1628
+ if (options.dpr) params.push(`dpr=${options.dpr}`);
1629
+ if (options.metadata) params.push(`metadata=${options.metadata}`);
1630
+ if (options.background) params.push(`background=${options.background}`);
1631
+ if (options.trim) {
1632
+ const trimParts = [];
1633
+ if (options.trim.top) trimParts.push(`${options.trim.top}`);
1634
+ if (options.trim.right) trimParts.push(`${options.trim.right}`);
1635
+ if (options.trim.bottom) trimParts.push(`${options.trim.bottom}`);
1636
+ if (options.trim.left) trimParts.push(`${options.trim.left}`);
1637
+ if (trimParts.length > 0) {
1638
+ params.push(`trim=${trimParts.join(";")}`);
1639
+ }
1640
+ }
1641
+ const queryString = params.join(",");
1642
+ return `/cdn-cgi/image/${queryString}/${sourceUrl}`;
1643
+ }
1644
+ function generateResponsiveUrls(baseUrl, mediaPath, sizes = DEFAULT_IMAGE_SIZES) {
1645
+ return {
1646
+ thumbnail: buildImageUrl(baseUrl, mediaPath, sizes.thumbnail),
1647
+ small: buildImageUrl(baseUrl, mediaPath, sizes.small),
1648
+ medium: buildImageUrl(baseUrl, mediaPath, sizes.medium),
1649
+ large: buildImageUrl(baseUrl, mediaPath, sizes.large),
1650
+ original: buildImageUrl(baseUrl, mediaPath, sizes.original)
1651
+ };
1652
+ }
1653
+ function generateSrcSet(baseUrl, mediaPath, widths = [400, 800, 1200, 1600]) {
1654
+ return widths.map((width) => {
1655
+ const url = buildImageUrl(baseUrl, mediaPath, {
1656
+ width,
1657
+ fit: "scale-down",
1658
+ quality: 85,
1659
+ format: "auto"
1660
+ });
1661
+ return `${url} ${width}w`;
1662
+ }).join(", ");
1663
+ }
1664
+ function generateSizesAttribute(breakpoints = [
1665
+ { maxWidth: "640px", size: "100vw" },
1666
+ { maxWidth: "768px", size: "80vw" },
1667
+ { maxWidth: "1024px", size: "60vw" },
1668
+ { maxWidth: "1280px", size: "50vw" }
1669
+ ]) {
1670
+ const mediaQueries = breakpoints.map(
1671
+ (bp) => `(max-width: ${bp.maxWidth}) ${bp.size}`
1672
+ );
1673
+ mediaQueries.push("40vw");
1674
+ return mediaQueries.join(", ");
1675
+ }
1676
+ function generateResponsiveImageHtml(baseUrl, mediaPath, alt, options) {
1677
+ const srcset = generateSrcSet(baseUrl, mediaPath, options?.widths);
1678
+ const sizes = options?.sizes || generateSizesAttribute();
1679
+ const src = buildImageUrl(baseUrl, mediaPath, {
1680
+ width: 800,
1681
+ fit: "scale-down",
1682
+ quality: 85,
1683
+ format: "auto"
1684
+ });
1685
+ return `<img
1686
+ src="${src}"
1687
+ srcset="${srcset}"
1688
+ sizes="${sizes}"
1689
+ alt="${alt}"
1690
+ ${options?.className ? `class="${options.className}"` : ""}
1691
+ ${options?.loading ? `loading="${options.loading}"` : 'loading="lazy"'}
1692
+ ${options?.decoding ? `decoding="${options.decoding}"` : 'decoding="async"'}
1693
+ />`;
1694
+ }
1695
+ function transformMediaItem(baseUrl, media, sizes) {
1696
+ let mediaPath;
1697
+ if ("link" in media) {
1698
+ mediaPath = media.link.replace(/^https?:\/\/[^/]+\//, "");
1699
+ } else {
1700
+ mediaPath = `media/${media.site_name}/${media.r2_key.split("/").pop()}`;
1701
+ }
1702
+ return generateResponsiveUrls(baseUrl, mediaPath, sizes);
1703
+ }
1704
+
1575
1705
  // src/loaders.ts
1576
1706
  var noopLogger = {};
1577
1707
  var log = (logger, level, ...args) => {
@@ -1968,6 +2098,7 @@ async function createCheckoutSession(options) {
1968
2098
  CheckoutClient,
1969
2099
  ContactClient,
1970
2100
  ContentClient,
2101
+ DEFAULT_IMAGE_SIZES,
1971
2102
  HttpClient,
1972
2103
  NewsletterClient,
1973
2104
  OrganizationsClient,
@@ -1975,9 +2106,14 @@ async function createCheckoutSession(options) {
1975
2106
  ProductsClient,
1976
2107
  SitesClient,
1977
2108
  WebhooksClient,
2109
+ buildImageUrl,
1978
2110
  createApiError,
1979
2111
  createCheckoutSession,
1980
2112
  createPerspectApiClient,
2113
+ generateResponsiveImageHtml,
2114
+ generateResponsiveUrls,
2115
+ generateSizesAttribute,
2116
+ generateSrcSet,
1981
2117
  loadAllContent,
1982
2118
  loadContentBySlug,
1983
2119
  loadPages,
@@ -1985,5 +2121,6 @@ async function createCheckoutSession(options) {
1985
2121
  loadProductBySlug,
1986
2122
  loadProducts,
1987
2123
  transformContent,
2124
+ transformMediaItem,
1988
2125
  transformProduct
1989
2126
  });
package/dist/index.mjs CHANGED
@@ -1521,6 +1521,129 @@ function createPerspectApiClient(config) {
1521
1521
  }
1522
1522
  var perspect_api_client_default = PerspectApiClient;
1523
1523
 
1524
+ // src/utils/image-transform.ts
1525
+ var DEFAULT_IMAGE_SIZES = {
1526
+ thumbnail: {
1527
+ width: 150,
1528
+ height: 150,
1529
+ fit: "cover",
1530
+ quality: 85,
1531
+ format: "auto"
1532
+ },
1533
+ small: {
1534
+ width: 400,
1535
+ fit: "scale-down",
1536
+ quality: 85,
1537
+ format: "auto"
1538
+ },
1539
+ medium: {
1540
+ width: 800,
1541
+ fit: "scale-down",
1542
+ quality: 85,
1543
+ format: "auto"
1544
+ },
1545
+ large: {
1546
+ width: 1200,
1547
+ fit: "scale-down",
1548
+ quality: 85,
1549
+ format: "auto"
1550
+ },
1551
+ original: {
1552
+ format: "auto"
1553
+ }
1554
+ };
1555
+ function buildImageUrl(baseUrl, mediaPath, options) {
1556
+ const sourceUrl = `${baseUrl}/${mediaPath.replace(/^\//, "")}`;
1557
+ if (!options || Object.keys(options).length === 0) {
1558
+ return sourceUrl;
1559
+ }
1560
+ const params = [];
1561
+ if (options.width) params.push(`width=${options.width}`);
1562
+ if (options.height) params.push(`height=${options.height}`);
1563
+ if (options.fit) params.push(`fit=${options.fit}`);
1564
+ if (options.gravity) params.push(`gravity=${options.gravity}`);
1565
+ if (options.quality) params.push(`quality=${options.quality}`);
1566
+ if (options.format) params.push(`format=${options.format}`);
1567
+ if (options.sharpen) params.push(`sharpen=${options.sharpen}`);
1568
+ if (options.blur) params.push(`blur=${options.blur}`);
1569
+ if (options.rotate) params.push(`rotate=${options.rotate}`);
1570
+ if (options.dpr) params.push(`dpr=${options.dpr}`);
1571
+ if (options.metadata) params.push(`metadata=${options.metadata}`);
1572
+ if (options.background) params.push(`background=${options.background}`);
1573
+ if (options.trim) {
1574
+ const trimParts = [];
1575
+ if (options.trim.top) trimParts.push(`${options.trim.top}`);
1576
+ if (options.trim.right) trimParts.push(`${options.trim.right}`);
1577
+ if (options.trim.bottom) trimParts.push(`${options.trim.bottom}`);
1578
+ if (options.trim.left) trimParts.push(`${options.trim.left}`);
1579
+ if (trimParts.length > 0) {
1580
+ params.push(`trim=${trimParts.join(";")}`);
1581
+ }
1582
+ }
1583
+ const queryString = params.join(",");
1584
+ return `/cdn-cgi/image/${queryString}/${sourceUrl}`;
1585
+ }
1586
+ function generateResponsiveUrls(baseUrl, mediaPath, sizes = DEFAULT_IMAGE_SIZES) {
1587
+ return {
1588
+ thumbnail: buildImageUrl(baseUrl, mediaPath, sizes.thumbnail),
1589
+ small: buildImageUrl(baseUrl, mediaPath, sizes.small),
1590
+ medium: buildImageUrl(baseUrl, mediaPath, sizes.medium),
1591
+ large: buildImageUrl(baseUrl, mediaPath, sizes.large),
1592
+ original: buildImageUrl(baseUrl, mediaPath, sizes.original)
1593
+ };
1594
+ }
1595
+ function generateSrcSet(baseUrl, mediaPath, widths = [400, 800, 1200, 1600]) {
1596
+ return widths.map((width) => {
1597
+ const url = buildImageUrl(baseUrl, mediaPath, {
1598
+ width,
1599
+ fit: "scale-down",
1600
+ quality: 85,
1601
+ format: "auto"
1602
+ });
1603
+ return `${url} ${width}w`;
1604
+ }).join(", ");
1605
+ }
1606
+ function generateSizesAttribute(breakpoints = [
1607
+ { maxWidth: "640px", size: "100vw" },
1608
+ { maxWidth: "768px", size: "80vw" },
1609
+ { maxWidth: "1024px", size: "60vw" },
1610
+ { maxWidth: "1280px", size: "50vw" }
1611
+ ]) {
1612
+ const mediaQueries = breakpoints.map(
1613
+ (bp) => `(max-width: ${bp.maxWidth}) ${bp.size}`
1614
+ );
1615
+ mediaQueries.push("40vw");
1616
+ return mediaQueries.join(", ");
1617
+ }
1618
+ function generateResponsiveImageHtml(baseUrl, mediaPath, alt, options) {
1619
+ const srcset = generateSrcSet(baseUrl, mediaPath, options?.widths);
1620
+ const sizes = options?.sizes || generateSizesAttribute();
1621
+ const src = buildImageUrl(baseUrl, mediaPath, {
1622
+ width: 800,
1623
+ fit: "scale-down",
1624
+ quality: 85,
1625
+ format: "auto"
1626
+ });
1627
+ return `<img
1628
+ src="${src}"
1629
+ srcset="${srcset}"
1630
+ sizes="${sizes}"
1631
+ alt="${alt}"
1632
+ ${options?.className ? `class="${options.className}"` : ""}
1633
+ ${options?.loading ? `loading="${options.loading}"` : 'loading="lazy"'}
1634
+ ${options?.decoding ? `decoding="${options.decoding}"` : 'decoding="async"'}
1635
+ />`;
1636
+ }
1637
+ function transformMediaItem(baseUrl, media, sizes) {
1638
+ let mediaPath;
1639
+ if ("link" in media) {
1640
+ mediaPath = media.link.replace(/^https?:\/\/[^/]+\//, "");
1641
+ } else {
1642
+ mediaPath = `media/${media.site_name}/${media.r2_key.split("/").pop()}`;
1643
+ }
1644
+ return generateResponsiveUrls(baseUrl, mediaPath, sizes);
1645
+ }
1646
+
1524
1647
  // src/loaders.ts
1525
1648
  var noopLogger = {};
1526
1649
  var log = (logger, level, ...args) => {
@@ -1916,6 +2039,7 @@ export {
1916
2039
  CheckoutClient,
1917
2040
  ContactClient,
1918
2041
  ContentClient,
2042
+ DEFAULT_IMAGE_SIZES,
1919
2043
  HttpClient,
1920
2044
  NewsletterClient,
1921
2045
  OrganizationsClient,
@@ -1923,10 +2047,15 @@ export {
1923
2047
  ProductsClient,
1924
2048
  SitesClient,
1925
2049
  WebhooksClient,
2050
+ buildImageUrl,
1926
2051
  createApiError,
1927
2052
  createCheckoutSession,
1928
2053
  createPerspectApiClient,
1929
2054
  perspect_api_client_default as default,
2055
+ generateResponsiveImageHtml,
2056
+ generateResponsiveUrls,
2057
+ generateSizesAttribute,
2058
+ generateSrcSet,
1930
2059
  loadAllContent,
1931
2060
  loadContentBySlug,
1932
2061
  loadPages,
@@ -1934,5 +2063,6 @@ export {
1934
2063
  loadProductBySlug,
1935
2064
  loadProducts,
1936
2065
  transformContent,
2066
+ transformMediaItem,
1937
2067
  transformProduct
1938
2068
  };
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.0",
4
4
  "description": "TypeScript SDK for PerspectAPI - Cloudflare Workers compatible",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
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,
@@ -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
+ }