@arraypress/media-url 1.0.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/package.json +15 -0
- package/src/index.d.ts +61 -0
- package/src/index.js +157 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arraypress/media-url",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Build media URLs with optional Cloudflare Image Resizing transforms.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": { ".": { "import": "./src/index.js", "types": "./src/index.d.ts" } },
|
|
9
|
+
"files": ["src"],
|
|
10
|
+
"scripts": { "test": "node --test tests/media-url.test.js" },
|
|
11
|
+
"keywords": ["media", "url", "cloudflare", "image-resizing", "cdn", "r2"],
|
|
12
|
+
"author": "David Sherlock",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": { "type": "git", "url": "https://github.com/arraypress/media-url" }
|
|
15
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface TransformOptions {
|
|
2
|
+
/** Target width in pixels. */
|
|
3
|
+
width?: number;
|
|
4
|
+
/** Target height in pixels. */
|
|
5
|
+
height?: number;
|
|
6
|
+
/** Quality 1-100 (default 80). */
|
|
7
|
+
quality?: number;
|
|
8
|
+
/** Fit mode: cover, contain, scale-down, crop, pad (default cover). */
|
|
9
|
+
fit?: string;
|
|
10
|
+
/** Output format: auto, webp, avif, json (default auto). */
|
|
11
|
+
format?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MediaUrlConfig {
|
|
15
|
+
/** Public base URL for direct access (e.g. "https://media.example.com"). */
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/** Enable Cloudflare Image Resizing (default false). */
|
|
18
|
+
resizing?: boolean;
|
|
19
|
+
/** Proxy fallback path prefix (default "/api/media"). */
|
|
20
|
+
proxyPath?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MediaUrlOptions {
|
|
24
|
+
/** Media ID (used for proxy fallback). */
|
|
25
|
+
id: string | number;
|
|
26
|
+
/** Storage object key (e.g. "media/abc.jpg"). */
|
|
27
|
+
fileKey: string;
|
|
28
|
+
/** Image transform options. */
|
|
29
|
+
transform?: TransformOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ImageRecord {
|
|
33
|
+
id: string | number;
|
|
34
|
+
file_key: string;
|
|
35
|
+
alt_text?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ImageResult {
|
|
39
|
+
id: string | number;
|
|
40
|
+
url: string;
|
|
41
|
+
thumbnail: string;
|
|
42
|
+
alt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AudioRecord {
|
|
46
|
+
media_id?: string | number;
|
|
47
|
+
external_url?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ThumbnailSizes {
|
|
51
|
+
thumbnail?: TransformOptions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Build a public URL for a media item with optional Cloudflare Image Resizing. */
|
|
55
|
+
export function mediaUrl(options: MediaUrlOptions, config?: MediaUrlConfig): string;
|
|
56
|
+
|
|
57
|
+
/** Build URLs for a list of images with multiple size variants. */
|
|
58
|
+
export function buildImageUrls(images: ImageRecord[], config?: MediaUrlConfig, sizes?: ThumbnailSizes): ImageResult[];
|
|
59
|
+
|
|
60
|
+
/** Resolve a URL for an audio file. */
|
|
61
|
+
export function buildAudioUrl(audio: AudioRecord, config?: MediaUrlConfig, mediaLookup?: Record<string | number, string>): string;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @arraypress/media-url
|
|
3
|
+
*
|
|
4
|
+
* Build media URLs with optional Cloudflare Image Resizing transforms.
|
|
5
|
+
*
|
|
6
|
+
* Three modes based on configuration:
|
|
7
|
+
* 1. Proxy fallback: /api/media/:id
|
|
8
|
+
* 2. Direct URL: https://media.example.com/media/abc.jpg
|
|
9
|
+
* 3. With resizing: /cdn-cgi/image/w=400,q=80,f=auto/https://media.example.com/media/abc.jpg
|
|
10
|
+
*
|
|
11
|
+
* Zero dependencies. Works in any JS runtime.
|
|
12
|
+
*
|
|
13
|
+
* @module @arraypress/media-url
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a public URL for a media item.
|
|
18
|
+
*
|
|
19
|
+
* When no baseUrl is provided, falls back to the proxyPath.
|
|
20
|
+
* When baseUrl is set but resizing is off, returns a direct URL.
|
|
21
|
+
* When both baseUrl and resizing are enabled, wraps the direct URL
|
|
22
|
+
* with Cloudflare Image Resizing.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {string|number} options.id - Media ID (used for proxy fallback).
|
|
26
|
+
* @param {string} options.fileKey - Storage object key (e.g. "media/abc.jpg").
|
|
27
|
+
* @param {Object} [options.transform] - Image transform options.
|
|
28
|
+
* @param {number} [options.transform.width] - Target width in pixels.
|
|
29
|
+
* @param {number} [options.transform.height] - Target height in pixels.
|
|
30
|
+
* @param {number} [options.transform.quality] - Quality 1-100 (default 80).
|
|
31
|
+
* @param {string} [options.transform.fit] - Fit mode: cover, contain, scale-down, crop, pad (default cover).
|
|
32
|
+
* @param {string} [options.transform.format] - Output format: auto, webp, avif, json (default auto).
|
|
33
|
+
* @param {Object} [config] - URL configuration.
|
|
34
|
+
* @param {string} [config.baseUrl] - Public base URL for direct access (e.g. "https://media.example.com").
|
|
35
|
+
* @param {boolean} [config.resizing] - Enable Cloudflare Image Resizing (default false).
|
|
36
|
+
* @param {string} [config.proxyPath] - Proxy fallback path prefix (default "/api/media").
|
|
37
|
+
* @returns {string} The resolved URL.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Proxy fallback (no baseUrl)
|
|
41
|
+
* mediaUrl({ id: 42, fileKey: 'media/photo.jpg' });
|
|
42
|
+
* // → '/api/media/42'
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Direct R2 URL
|
|
46
|
+
* mediaUrl(
|
|
47
|
+
* { id: 42, fileKey: 'media/photo.jpg' },
|
|
48
|
+
* { baseUrl: 'https://media.example.com' }
|
|
49
|
+
* );
|
|
50
|
+
* // → 'https://media.example.com/media/photo.jpg'
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // With Cloudflare Image Resizing
|
|
54
|
+
* mediaUrl(
|
|
55
|
+
* { id: 42, fileKey: 'media/photo.jpg', transform: { width: 400 } },
|
|
56
|
+
* { baseUrl: 'https://media.example.com', resizing: true }
|
|
57
|
+
* );
|
|
58
|
+
* // → '/cdn-cgi/image/w=400,q=80,f=auto,fit=cover/https://media.example.com/media/photo.jpg'
|
|
59
|
+
*/
|
|
60
|
+
export function mediaUrl({ id, fileKey, transform }, config = {}) {
|
|
61
|
+
const baseUrl = config.baseUrl || '';
|
|
62
|
+
const resizing = config.resizing === true;
|
|
63
|
+
const proxyPath = config.proxyPath || '/api/media';
|
|
64
|
+
|
|
65
|
+
// No base URL — use proxy fallback
|
|
66
|
+
if (!baseUrl) {
|
|
67
|
+
return `${proxyPath}/${id}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build the direct URL
|
|
71
|
+
const directUrl = `${baseUrl.replace(/\/$/, '')}/${fileKey}`;
|
|
72
|
+
|
|
73
|
+
// No resizing or no transform — return direct URL
|
|
74
|
+
if (!resizing || !transform) {
|
|
75
|
+
return directUrl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build Cloudflare Image Resizing URL
|
|
79
|
+
const params = [];
|
|
80
|
+
if (transform.width) params.push(`w=${transform.width}`);
|
|
81
|
+
if (transform.height) params.push(`h=${transform.height}`);
|
|
82
|
+
params.push(`q=${transform.quality || 80}`);
|
|
83
|
+
params.push(`f=${transform.format || 'auto'}`);
|
|
84
|
+
params.push(`fit=${transform.fit || 'cover'}`);
|
|
85
|
+
|
|
86
|
+
return `/cdn-cgi/image/${params.join(',')}/${directUrl}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build URLs for a list of images with multiple size variants.
|
|
91
|
+
*
|
|
92
|
+
* Takes raw image records and returns objects with full URL,
|
|
93
|
+
* thumbnail URL, and alt text.
|
|
94
|
+
*
|
|
95
|
+
* @param {Array<Object>} images - Image records with id, file_key, and optional alt_text.
|
|
96
|
+
* @param {Object} [config] - URL configuration (same as mediaUrl config).
|
|
97
|
+
* @param {Object} [sizes] - Size configuration.
|
|
98
|
+
* @param {Object} [sizes.thumbnail] - Thumbnail transform (default { width: 400, quality: 80 }).
|
|
99
|
+
* @returns {Array<Object>} Image objects with id, url, thumbnail, and alt.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const images = buildImageUrls(
|
|
103
|
+
* [{ id: 1, file_key: 'media/photo.jpg', alt_text: 'A photo' }],
|
|
104
|
+
* { baseUrl: 'https://media.example.com', resizing: true }
|
|
105
|
+
* );
|
|
106
|
+
* // [{ id: 1, url: 'https://...', thumbnail: '/cdn-cgi/...', alt: 'A photo' }]
|
|
107
|
+
*/
|
|
108
|
+
export function buildImageUrls(images, config = {}, sizes = {}) {
|
|
109
|
+
const thumbnailTransform = sizes.thumbnail || { width: 400, quality: 80 };
|
|
110
|
+
|
|
111
|
+
return images.map(img => {
|
|
112
|
+
const base = { id: img.id, fileKey: img.file_key };
|
|
113
|
+
return {
|
|
114
|
+
id: img.id,
|
|
115
|
+
url: mediaUrl(base, config),
|
|
116
|
+
thumbnail: mediaUrl({ ...base, transform: thumbnailTransform }, config),
|
|
117
|
+
alt: img.alt_text || '',
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a URL for an audio file.
|
|
124
|
+
*
|
|
125
|
+
* Audio files are never resized. If the item has no media_id,
|
|
126
|
+
* falls back to its external_url.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} audio - Audio record with media_id and/or external_url.
|
|
129
|
+
* @param {Object} [config] - URL configuration (same as mediaUrl config).
|
|
130
|
+
* @param {Object} [mediaLookup] - Map of media_id → file_key for resolving storage keys.
|
|
131
|
+
* @returns {string} The audio URL.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // External URL (no media_id)
|
|
135
|
+
* buildAudioUrl({ external_url: 'https://cdn.example.com/song.mp3' });
|
|
136
|
+
* // → 'https://cdn.example.com/song.mp3'
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // R2-hosted audio
|
|
140
|
+
* buildAudioUrl(
|
|
141
|
+
* { media_id: 5 },
|
|
142
|
+
* { baseUrl: 'https://media.example.com' },
|
|
143
|
+
* { 5: 'media/song.mp3' }
|
|
144
|
+
* );
|
|
145
|
+
* // → 'https://media.example.com/media/song.mp3'
|
|
146
|
+
*/
|
|
147
|
+
export function buildAudioUrl(audio, config = {}, mediaLookup = {}) {
|
|
148
|
+
if (!audio.media_id) return audio.external_url || '';
|
|
149
|
+
|
|
150
|
+
const fileKey = mediaLookup[audio.media_id];
|
|
151
|
+
if (!fileKey) {
|
|
152
|
+
const proxyPath = config.proxyPath || '/api/media';
|
|
153
|
+
return `${proxyPath}/${audio.media_id}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return mediaUrl({ id: audio.media_id, fileKey }, config);
|
|
157
|
+
}
|