@howells/stow-next 0.1.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 +183 -0
- package/dist/chunk-DZDX7UVD.mjs +39 -0
- package/dist/chunk-HRAABMBJ.mjs +87 -0
- package/dist/chunk-HYKV6KDZ.mjs +31 -0
- package/dist/chunk-WN3NWAEN.mjs +93 -0
- package/dist/chunk-WWNT32U5.mjs +62 -0
- package/dist/chunk-X2Y24T4H.mjs +31 -0
- package/dist/image-loader.d.mts +119 -0
- package/dist/image-loader.d.ts +119 -0
- package/dist/image-loader.js +118 -0
- package/dist/image-loader.mjs +8 -0
- package/dist/index.d.mts +156 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +383 -0
- package/dist/index.mjs +267 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @howells/stow-next
|
|
2
|
+
|
|
3
|
+
Next.js integration for [Stow](https://stow.sh) file storage. Includes route handler helpers and an image loader for `next/image`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @howells/stow-next @howells/stow-server
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @howells/stow-next @howells/stow-server
|
|
11
|
+
# or
|
|
12
|
+
yarn add @howells/stow-next @howells/stow-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Create an upload route
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// app/api/upload/route.ts
|
|
21
|
+
import { createUploadHandler } from "@howells/stow-next";
|
|
22
|
+
import { StowServer } from "@howells/stow-server";
|
|
23
|
+
|
|
24
|
+
const stow = new StowServer(process.env.STOW_API_KEY!);
|
|
25
|
+
|
|
26
|
+
export const POST = createUploadHandler({
|
|
27
|
+
stow,
|
|
28
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
29
|
+
allowedTypes: ["image/*", "application/pdf"],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Use with @howells/stow-react
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
// app/page.tsx
|
|
37
|
+
import { UploadDropzone } from "@howells/stow-react";
|
|
38
|
+
|
|
39
|
+
export default function Page() {
|
|
40
|
+
return (
|
|
41
|
+
<UploadDropzone
|
|
42
|
+
endpoint="/api/upload"
|
|
43
|
+
onUploadComplete={(files) => console.log(files)}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Route Handler
|
|
50
|
+
|
|
51
|
+
### `createUploadHandler(config)`
|
|
52
|
+
|
|
53
|
+
Creates a Next.js route handler for file uploads.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createUploadHandler } from "@howells/stow-next";
|
|
57
|
+
|
|
58
|
+
export const POST = createUploadHandler({
|
|
59
|
+
// Required: Stow server instance
|
|
60
|
+
stow: new StowServer(process.env.STOW_API_KEY!),
|
|
61
|
+
|
|
62
|
+
// Optional: File restrictions
|
|
63
|
+
maxSize: 10 * 1024 * 1024, // Max file size in bytes
|
|
64
|
+
allowedTypes: ["image/*", ".pdf"], // Allowed MIME types or extensions
|
|
65
|
+
route: "uploads", // Default route/folder
|
|
66
|
+
|
|
67
|
+
// Optional: Custom validation
|
|
68
|
+
validate: async (file) => {
|
|
69
|
+
if (file.name.includes("secret")) {
|
|
70
|
+
return "Filename not allowed";
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Optional: Lifecycle hooks
|
|
76
|
+
onUploadBegin: async (file) => {
|
|
77
|
+
console.log(`Starting upload: ${file.name}`);
|
|
78
|
+
},
|
|
79
|
+
onUploadComplete: async (result) => {
|
|
80
|
+
console.log(`Uploaded: ${result.url}`);
|
|
81
|
+
// Save to database, etc.
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Image Loader
|
|
87
|
+
|
|
88
|
+
Use Stow's image transformation with Next.js Image component.
|
|
89
|
+
|
|
90
|
+
### Setup
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// next.config.js
|
|
94
|
+
module.exports = {
|
|
95
|
+
images: {
|
|
96
|
+
loader: "custom",
|
|
97
|
+
loaderFile: "./lib/stow-loader.ts",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// lib/stow-loader.ts
|
|
104
|
+
import { stowLoader } from "@howells/stow-next/image-loader";
|
|
105
|
+
export default stowLoader;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Usage
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import Image from "next/image";
|
|
112
|
+
|
|
113
|
+
function MyComponent() {
|
|
114
|
+
return (
|
|
115
|
+
<Image
|
|
116
|
+
src="https://stow.sh/files/bucket-id/image.jpg"
|
|
117
|
+
alt="My image"
|
|
118
|
+
width={800}
|
|
119
|
+
height={600}
|
|
120
|
+
quality={80}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Loader Config
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// lib/stow-loader.ts
|
|
130
|
+
import { createStowLoader } from "@howells/stow-next/image-loader";
|
|
131
|
+
|
|
132
|
+
export default createStowLoader({
|
|
133
|
+
baseUrl: "https://stow.sh",
|
|
134
|
+
defaultQuality: 80,
|
|
135
|
+
defaultFormat: "webp",
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## TypeScript
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import type {
|
|
143
|
+
UploadHandlerConfig,
|
|
144
|
+
StowLoaderConfig,
|
|
145
|
+
} from "@howells/stow-next";
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Complete Example
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// app/api/upload/route.ts
|
|
152
|
+
import { createUploadHandler } from "@howells/stow-next";
|
|
153
|
+
import { StowServer } from "@howells/stow-server";
|
|
154
|
+
import { db } from "@/lib/db";
|
|
155
|
+
import { auth } from "@/lib/auth";
|
|
156
|
+
|
|
157
|
+
const stow = new StowServer(process.env.STOW_API_KEY!);
|
|
158
|
+
|
|
159
|
+
export const POST = createUploadHandler({
|
|
160
|
+
stow,
|
|
161
|
+
maxSize: 5 * 1024 * 1024,
|
|
162
|
+
allowedTypes: ["image/jpeg", "image/png", "image/webp"],
|
|
163
|
+
|
|
164
|
+
validate: async (file) => {
|
|
165
|
+
const session = await auth();
|
|
166
|
+
if (!session) return "Unauthorized";
|
|
167
|
+
return true;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
onUploadComplete: async (result) => {
|
|
171
|
+
const session = await auth();
|
|
172
|
+
await db.insert(files).values({
|
|
173
|
+
key: result.key,
|
|
174
|
+
url: result.url,
|
|
175
|
+
userId: session!.user.id,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
function createStowLoader(config = {}) {
|
|
3
|
+
const {
|
|
4
|
+
baseUrl = "https://stow.sh",
|
|
5
|
+
defaultQuality = 75,
|
|
6
|
+
defaultFormat
|
|
7
|
+
} = config;
|
|
8
|
+
return function stowLoader2({
|
|
9
|
+
src,
|
|
10
|
+
width,
|
|
11
|
+
quality
|
|
12
|
+
}) {
|
|
13
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
14
|
+
const url = new URL(src, baseUrl);
|
|
15
|
+
const pathname = url.pathname;
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
params.set("w", width.toString());
|
|
18
|
+
params.set("q", (quality || defaultQuality).toString());
|
|
19
|
+
if (defaultFormat) {
|
|
20
|
+
params.set("f", defaultFormat);
|
|
21
|
+
}
|
|
22
|
+
if (pathname.startsWith("/files/")) {
|
|
23
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
24
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
25
|
+
}
|
|
26
|
+
if (pathname.startsWith("/transform/")) {
|
|
27
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
28
|
+
}
|
|
29
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
return src;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
var stowLoader = createStowLoader();
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
createStowLoader,
|
|
38
|
+
stowLoader
|
|
39
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
var STOW_DOMAIN_PATTERN = /\.stow\.sh$/;
|
|
3
|
+
function isStowUrl(src) {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(src);
|
|
6
|
+
return STOW_DOMAIN_PATTERN.test(url.hostname);
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function buildTransformParams(width, quality, config) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
params.set("w", width.toString());
|
|
14
|
+
if (config.aspectRatio) {
|
|
15
|
+
params.set("h", Math.round(width / config.aspectRatio).toString());
|
|
16
|
+
}
|
|
17
|
+
params.set("q", quality.toString());
|
|
18
|
+
if (config.fit) {
|
|
19
|
+
params.set("fit", config.fit);
|
|
20
|
+
}
|
|
21
|
+
if (config.gravity) {
|
|
22
|
+
params.set("gravity", config.gravity);
|
|
23
|
+
}
|
|
24
|
+
if (config.defaultFormat) {
|
|
25
|
+
params.set("f", config.defaultFormat);
|
|
26
|
+
}
|
|
27
|
+
return params;
|
|
28
|
+
}
|
|
29
|
+
function transformStowUrl(src, baseUrl, params) {
|
|
30
|
+
const url = new URL(src, baseUrl);
|
|
31
|
+
const pathname = url.pathname;
|
|
32
|
+
if (pathname.startsWith("/files/")) {
|
|
33
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
34
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
35
|
+
}
|
|
36
|
+
if (pathname.startsWith("/transform/")) {
|
|
37
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
38
|
+
}
|
|
39
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
40
|
+
}
|
|
41
|
+
function createStowLoader(config = {}) {
|
|
42
|
+
const {
|
|
43
|
+
baseUrl = "https://stow.sh",
|
|
44
|
+
defaultQuality = 75,
|
|
45
|
+
defaultFormat,
|
|
46
|
+
proxySlug,
|
|
47
|
+
fit,
|
|
48
|
+
gravity,
|
|
49
|
+
aspectRatio
|
|
50
|
+
} = config;
|
|
51
|
+
return function stowLoader2({
|
|
52
|
+
src,
|
|
53
|
+
width,
|
|
54
|
+
quality
|
|
55
|
+
}) {
|
|
56
|
+
const resolvedQuality = quality || defaultQuality;
|
|
57
|
+
const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
|
|
58
|
+
if (isStowUrl(src)) {
|
|
59
|
+
const params = buildTransformParams(width, resolvedQuality, paramConfig);
|
|
60
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
63
|
+
const params = buildTransformParams(width, resolvedQuality, paramConfig);
|
|
64
|
+
return transformStowUrl(src, baseUrl, params);
|
|
65
|
+
}
|
|
66
|
+
if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
params.set("url", src);
|
|
69
|
+
params.set("w", width.toString());
|
|
70
|
+
if (aspectRatio) {
|
|
71
|
+
params.set("h", Math.round(width / aspectRatio).toString());
|
|
72
|
+
}
|
|
73
|
+
params.set("q", resolvedQuality.toString());
|
|
74
|
+
if (fit) params.set("fit", fit);
|
|
75
|
+
if (gravity) params.set("gravity", gravity);
|
|
76
|
+
if (defaultFormat) params.set("f", defaultFormat);
|
|
77
|
+
return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
|
|
78
|
+
}
|
|
79
|
+
return src;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
var stowLoader = createStowLoader();
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
createStowLoader,
|
|
86
|
+
stowLoader
|
|
87
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
function createObliqLoader(config = {}) {
|
|
3
|
+
const { baseUrl = "https://obliq.co", defaultQuality = 75, defaultFormat } = config;
|
|
4
|
+
return function obliqLoader2({ src, width, quality }) {
|
|
5
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
6
|
+
const url = new URL(src, baseUrl);
|
|
7
|
+
const pathname = url.pathname;
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
params.set("w", width.toString());
|
|
10
|
+
params.set("q", (quality || defaultQuality).toString());
|
|
11
|
+
if (defaultFormat) {
|
|
12
|
+
params.set("f", defaultFormat);
|
|
13
|
+
}
|
|
14
|
+
if (pathname.startsWith("/files/")) {
|
|
15
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
16
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
if (pathname.startsWith("/transform/")) {
|
|
19
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
20
|
+
}
|
|
21
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
return src;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var obliqLoader = createObliqLoader();
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
createObliqLoader,
|
|
30
|
+
obliqLoader
|
|
31
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
var STOW_DOMAIN_PATTERN = /\.stow\.sh$/;
|
|
3
|
+
function isStowUrl(src) {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(src);
|
|
6
|
+
return STOW_DOMAIN_PATTERN.test(url.hostname);
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function buildTransformParams(width, quality, config) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
params.set("w", width.toString());
|
|
14
|
+
if (config.aspectRatio) {
|
|
15
|
+
params.set("h", Math.round(width / config.aspectRatio).toString());
|
|
16
|
+
}
|
|
17
|
+
params.set("q", quality.toString());
|
|
18
|
+
if (config.fit) {
|
|
19
|
+
params.set("fit", config.fit);
|
|
20
|
+
}
|
|
21
|
+
if (config.gravity) {
|
|
22
|
+
params.set("gravity", config.gravity);
|
|
23
|
+
}
|
|
24
|
+
if (config.defaultFormat) {
|
|
25
|
+
params.set("f", config.defaultFormat);
|
|
26
|
+
}
|
|
27
|
+
return params;
|
|
28
|
+
}
|
|
29
|
+
function transformStowUrl(src, baseUrl, params) {
|
|
30
|
+
const url = new URL(src, baseUrl);
|
|
31
|
+
const pathname = url.pathname;
|
|
32
|
+
if (pathname.startsWith("/files/")) {
|
|
33
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
34
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
35
|
+
}
|
|
36
|
+
if (pathname.startsWith("/transform/")) {
|
|
37
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
38
|
+
}
|
|
39
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
40
|
+
}
|
|
41
|
+
function createStowLoader(config = {}) {
|
|
42
|
+
const {
|
|
43
|
+
baseUrl = "https://stow.sh",
|
|
44
|
+
defaultQuality = 75,
|
|
45
|
+
defaultFormat,
|
|
46
|
+
proxySlug,
|
|
47
|
+
fit,
|
|
48
|
+
gravity,
|
|
49
|
+
aspectRatio
|
|
50
|
+
} = config;
|
|
51
|
+
return function stowLoader2({
|
|
52
|
+
src,
|
|
53
|
+
width,
|
|
54
|
+
quality
|
|
55
|
+
}) {
|
|
56
|
+
const resolvedQuality = quality || defaultQuality;
|
|
57
|
+
const paramConfig = { defaultFormat, fit, gravity, aspectRatio };
|
|
58
|
+
if (isStowUrl(src)) {
|
|
59
|
+
const params = buildTransformParams(width, resolvedQuality, paramConfig);
|
|
60
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
61
|
+
}
|
|
62
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
63
|
+
const params = buildTransformParams(width, resolvedQuality, paramConfig);
|
|
64
|
+
return transformStowUrl(src, baseUrl, params);
|
|
65
|
+
}
|
|
66
|
+
if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
params.set("url", src);
|
|
69
|
+
params.set("w", width.toString());
|
|
70
|
+
if (aspectRatio) {
|
|
71
|
+
params.set("h", Math.round(width / aspectRatio).toString());
|
|
72
|
+
}
|
|
73
|
+
params.set("q", resolvedQuality.toString());
|
|
74
|
+
if (fit) {
|
|
75
|
+
params.set("fit", fit);
|
|
76
|
+
}
|
|
77
|
+
if (gravity) {
|
|
78
|
+
params.set("gravity", gravity);
|
|
79
|
+
}
|
|
80
|
+
if (defaultFormat) {
|
|
81
|
+
params.set("f", defaultFormat);
|
|
82
|
+
}
|
|
83
|
+
return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
|
|
84
|
+
}
|
|
85
|
+
return src;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
var stowLoader = createStowLoader();
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
createStowLoader,
|
|
92
|
+
stowLoader
|
|
93
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
function buildTransformParams(width, quality, defaultFormat) {
|
|
3
|
+
const params = new URLSearchParams();
|
|
4
|
+
params.set("w", width.toString());
|
|
5
|
+
params.set("q", quality.toString());
|
|
6
|
+
if (defaultFormat) {
|
|
7
|
+
params.set("f", defaultFormat);
|
|
8
|
+
}
|
|
9
|
+
return params;
|
|
10
|
+
}
|
|
11
|
+
function transformStowUrl(src, baseUrl, params) {
|
|
12
|
+
const url = new URL(src, baseUrl);
|
|
13
|
+
const pathname = url.pathname;
|
|
14
|
+
if (pathname.startsWith("/files/")) {
|
|
15
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
16
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
if (pathname.startsWith("/transform/")) {
|
|
19
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
20
|
+
}
|
|
21
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
function createStowLoader(config = {}) {
|
|
24
|
+
const {
|
|
25
|
+
baseUrl = "https://stow.sh",
|
|
26
|
+
defaultQuality = 75,
|
|
27
|
+
defaultFormat,
|
|
28
|
+
proxySlug
|
|
29
|
+
} = config;
|
|
30
|
+
return function stowLoader2({
|
|
31
|
+
src,
|
|
32
|
+
width,
|
|
33
|
+
quality
|
|
34
|
+
}) {
|
|
35
|
+
const resolvedQuality = quality || defaultQuality;
|
|
36
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
37
|
+
const params = buildTransformParams(
|
|
38
|
+
width,
|
|
39
|
+
resolvedQuality,
|
|
40
|
+
defaultFormat
|
|
41
|
+
);
|
|
42
|
+
return transformStowUrl(src, baseUrl, params);
|
|
43
|
+
}
|
|
44
|
+
if (proxySlug && (src.startsWith("http://") || src.startsWith("https://"))) {
|
|
45
|
+
const params = new URLSearchParams();
|
|
46
|
+
params.set("url", src);
|
|
47
|
+
params.set("w", width.toString());
|
|
48
|
+
params.set("q", resolvedQuality.toString());
|
|
49
|
+
if (defaultFormat) {
|
|
50
|
+
params.set("f", defaultFormat);
|
|
51
|
+
}
|
|
52
|
+
return `https://proxy.stow.sh/${proxySlug}/?${params.toString()}`;
|
|
53
|
+
}
|
|
54
|
+
return src;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
var stowLoader = createStowLoader();
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
createStowLoader,
|
|
61
|
+
stowLoader
|
|
62
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/image-loader.ts
|
|
2
|
+
function createStowLoader(config = {}) {
|
|
3
|
+
const { baseUrl = "https://stow.sh", defaultQuality = 75, defaultFormat } = config;
|
|
4
|
+
return function stowLoader2({ src, width, quality }) {
|
|
5
|
+
if (src.startsWith(baseUrl) || src.startsWith("/files/")) {
|
|
6
|
+
const url = new URL(src, baseUrl);
|
|
7
|
+
const pathname = url.pathname;
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
params.set("w", width.toString());
|
|
10
|
+
params.set("q", (quality || defaultQuality).toString());
|
|
11
|
+
if (defaultFormat) {
|
|
12
|
+
params.set("f", defaultFormat);
|
|
13
|
+
}
|
|
14
|
+
if (pathname.startsWith("/files/")) {
|
|
15
|
+
const transformPath = pathname.replace("/files/", "/transform/");
|
|
16
|
+
return `${baseUrl}${transformPath}?${params.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
if (pathname.startsWith("/transform/")) {
|
|
19
|
+
return `${baseUrl}${pathname}?${params.toString()}`;
|
|
20
|
+
}
|
|
21
|
+
return `${src}${src.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
return src;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var stowLoader = createStowLoader();
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
createStowLoader,
|
|
30
|
+
stowLoader
|
|
31
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stow Image Loader for Next.js
|
|
3
|
+
*
|
|
4
|
+
* Use Stow's image transformation service with Next.js Image component.
|
|
5
|
+
* Supports both vanity subdomain URLs ({slug}.stow.sh/{key}) and
|
|
6
|
+
* legacy path-based URLs (stow.sh/files/{bucketId}/{key}).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // next.config.js — global loader
|
|
11
|
+
* module.exports = {
|
|
12
|
+
* images: {
|
|
13
|
+
* loader: "custom",
|
|
14
|
+
* loaderFile: "./lib/stow-loader.ts",
|
|
15
|
+
* },
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* // lib/stow-loader.ts
|
|
19
|
+
* import { stowLoader } from "@howells/stow-next/image-loader";
|
|
20
|
+
* export default stowLoader;
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // Per-component loader with crop options
|
|
26
|
+
* import { createStowLoader } from "@howells/stow-next/image-loader";
|
|
27
|
+
* import Image from "next/image";
|
|
28
|
+
*
|
|
29
|
+
* const avatarLoader = createStowLoader({
|
|
30
|
+
* fit: "cover",
|
|
31
|
+
* gravity: "face",
|
|
32
|
+
* aspectRatio: 1,
|
|
33
|
+
* defaultFormat: "webp",
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* function Avatar({ src }: { src: string }) {
|
|
37
|
+
* return <Image loader={avatarLoader} src={src} alt="Avatar" width={128} height={128} />;
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
interface ImageLoaderProps {
|
|
42
|
+
quality?: number;
|
|
43
|
+
src: string;
|
|
44
|
+
width: number;
|
|
45
|
+
}
|
|
46
|
+
interface StowLoaderConfig {
|
|
47
|
+
/**
|
|
48
|
+
* Fixed aspect ratio (width / height).
|
|
49
|
+
* When set, height is calculated from the requested width.
|
|
50
|
+
* Example: 1 for square, 16/9 for widescreen, 4/5 for portrait.
|
|
51
|
+
*/
|
|
52
|
+
aspectRatio?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Base URL for the Stow service.
|
|
55
|
+
* @default "https://stow.sh"
|
|
56
|
+
*/
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Default output format.
|
|
60
|
+
*/
|
|
61
|
+
defaultFormat?: "webp" | "avif" | "jpeg" | "png";
|
|
62
|
+
/**
|
|
63
|
+
* Default image quality (1-100).
|
|
64
|
+
* @default 75
|
|
65
|
+
*/
|
|
66
|
+
defaultQuality?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Resize fit mode (Cloudflare Images).
|
|
69
|
+
* - "scale-down" — shrink to fit, never enlarge
|
|
70
|
+
* - "contain" — fit within bounds preserving aspect ratio
|
|
71
|
+
* - "cover" — fill bounds, crop excess
|
|
72
|
+
* - "crop" — crop to exact dimensions
|
|
73
|
+
* - "pad" — fit within bounds, pad remaining space
|
|
74
|
+
*/
|
|
75
|
+
fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
|
|
76
|
+
/**
|
|
77
|
+
* Crop gravity / anchor point.
|
|
78
|
+
* - "auto" — subject-aware ML cropping
|
|
79
|
+
* - "face" — face-detection cropping
|
|
80
|
+
* - directional: "left", "right", "top", "bottom", "center"
|
|
81
|
+
*/
|
|
82
|
+
gravity?: "auto" | "face" | "left" | "right" | "top" | "bottom" | "center";
|
|
83
|
+
/**
|
|
84
|
+
* Bucket slug for proxying external images through Stow.
|
|
85
|
+
* When set, external URLs (http/https) are routed through proxy.stow.sh
|
|
86
|
+
* for R2 caching and on-the-fly image transforms.
|
|
87
|
+
*/
|
|
88
|
+
proxySlug?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a custom image loader with configuration.
|
|
92
|
+
*
|
|
93
|
+
* Returns a stable function reference safe for use as a Next.js `loader` prop.
|
|
94
|
+
*/
|
|
95
|
+
declare function createStowLoader(config?: StowLoaderConfig): ({ src, width, quality, }: ImageLoaderProps) => string;
|
|
96
|
+
/**
|
|
97
|
+
* Default Stow image loader.
|
|
98
|
+
*
|
|
99
|
+
* Handles vanity subdomain URLs ({slug}.stow.sh) and legacy
|
|
100
|
+
* path-based URLs (stow.sh/files/...). Appends width and quality
|
|
101
|
+
* as transform query params.
|
|
102
|
+
*
|
|
103
|
+
* ```js
|
|
104
|
+
* // next.config.js
|
|
105
|
+
* module.exports = {
|
|
106
|
+
* images: {
|
|
107
|
+
* loader: "custom",
|
|
108
|
+
* loaderFile: "./lib/stow-loader.ts",
|
|
109
|
+
* },
|
|
110
|
+
* };
|
|
111
|
+
*
|
|
112
|
+
* // lib/stow-loader.ts
|
|
113
|
+
* import { stowLoader } from "@howells/stow-next/image-loader";
|
|
114
|
+
* export default stowLoader;
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare const stowLoader: ({ src, width, quality, }: ImageLoaderProps) => string;
|
|
118
|
+
|
|
119
|
+
export { type ImageLoaderProps, type StowLoaderConfig, createStowLoader, stowLoader };
|