Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions docs/content/3.providers/flyimg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
````mdc
---
title: Flyimg
description: Nuxt Image has first class integration with Flyimg.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/image/blob/main/src/runtime/providers/flyimg.ts
size: xs
---

Integration between [Flyimg](https://flyimg.io) and the image module.

[Flyimg](https://github.com/flyimg/flyimg) is a self-hosted, open-source image processing server built on ImageMagick. It accepts images from a source URL and applies transformations on the fly, caching results for subsequent requests. A managed SaaS version is also available at [flyimg.io](https://flyimg.io).

## Setup

### Self‑hosted

`baseURL` points to your **Flyimg server**. If you use relative image paths (e.g. `<NuxtImg src="/images/photo.jpg" />`), also set `sourceURL` to your **website's public URL** so that Flyimg can fetch the source image — it requires an absolute URL. Absolute `src` values (e.g. from a CDN) are passed through as-is and `sourceURL` is ignored for those.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
image: {
flyimg: {
// URL of your Flyimg server
baseURL: 'https://flyimg.example.com',
// Public URL of your website — only needed for relative image paths
sourceURL: 'https://www.example.com',
}
}
})
```

::callout{type="warning"}
**nginx users:** nginx's default `merge_slashes on` setting collapses consecutive slashes in URLs, which corrupts the embedded `https://` in Flyimg request paths (e.g. `https://flyimg.example.com/upload/-/https://www.example.com/photo.jpg`). Add `merge_slashes off;` to the `server` block of your nginx config when reverse-proxying requests to Flyimg.
::

### Flyimg SaaS

After [creating an instance](https://flyimg.io/documentation) you receive a unique subdomain (e.g. `img-abc123.flyimg.io`):

```ts [nuxt.config.ts]
export default defineNuxtConfig({
image: {
flyimg: {
// Unique subdomain provided by Flyimg SaaS
baseURL: 'https://img-abc123.flyimg.io',
// Public URL of your website — only needed for relative image paths
sourceURL: 'https://www.example.com',
}
}
})
```

## Options

| Option | Default | Description |
|---|---|---|
| `baseURL` | — | **Required.** URL of your Flyimg server or SaaS instance. |
| `sourceURL` | — | Public base URL of your website. Only used for relative image paths — prefixed to make them absolute before passing to Flyimg. Absolute `src` values are unaffected. |
| `processType` | `upload` | Flyimg process type: `upload` (serve the image) or `path` (return the path as text). |

## Modifiers

In addition to the standard `width`, `height`, `quality`, `format`, and `fit` modifiers, the Flyimg provider exposes the full range of [Flyimg URL options](https://docs.flyimg.io/url-options/).

### `fit`

| Nuxt Image value | Flyimg behaviour |
|---|---|
| `contain` / `inside` | Scale to fit within the target rectangle preserving aspect ratio (Flyimg default) |
| `cover` | Crop to fill the rectangle (`c_1`) |
| `fill` | Stretch to fill without preserving aspect ratio (`par_0`) |
| `outside` | ⚠️ Unsupported — ignored (dev-time warning emitted) |

### Additional Flyimg modifiers

```vue
<NuxtImg
provider="flyimg"
src="/photo.jpg"
width="800"
:modifiers="{
gravity: 'NorthEast',
sharpen: '0x5',
background: '#ffffff',
strip: false,
}"
/>
```

Refer to the [Flyimg URL options documentation](https://docs.flyimg.io/url-options/) for the full list of supported parameters.
````
25 changes: 25 additions & 0 deletions playground/app/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,31 @@ export const providers: Provider[] = [
},
],
},
// Flyimg
{
name: 'flyimg',
samples: [
{
src: 'https://picsum.photos/seed/nuxtimage/800/600',
width: 500,
height: 375,
quality: 80,
format: 'webp',
},
{
src: 'https://picsum.photos/seed/nuxtimage/800/600',
width: 300,
height: 200,
fit: 'cover',
quality: 75,
},
{
src: 'https://picsum.photos/seed/nuxtimage/800/600',
width: 200,
fit: 'contain',
},
],
},
// ImageKit
{
name: 'imagekit',
Expand Down
4 changes: 4 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export default defineNuxtConfig({
filerobot: {
baseURL: 'https://fesrinkeb.filerobot.com/',
},
flyimg: {
baseURL: 'https://demo.flyimg.io',
sourceURL: 'https://picsum.photos',
},
github: {},
glide: {
baseURL: 'https://glide.herokuapp.com/1.0/',
Expand Down
1 change: 1 addition & 0 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const BuiltInProviders = [
'directus',
'fastly',
'filerobot',
'flyimg',
'github',
'glide',
'gumlet',
Expand Down
229 changes: 229 additions & 0 deletions src/runtime/providers/flyimg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// https://docs.flyimg.io/url-options/

import { joinURL, hasProtocol } from 'ufo'
import { createOperationsGenerator } from '../utils/index'
import { defineProvider } from '../utils/provider'

/**
* Flyimg URL format:
* https://flyimg.example.com/{processType}/{image_options}/{path_to_image}
*
* Example:
* https://demo.flyimg.io/upload/w_300,h_200,q_85/https://example.com/image.jpg
*/
const operationsGenerator = createOperationsGenerator({
keyMap: {
// Core dimensions
width: 'w',
height: 'h',
quality: 'q',
format: 'o',
rotate: 'r',

// Cropping
crop: 'c',
gravity: 'g',

// WebP
webpLossless: 'webpl',
webpMethod: 'webpm',

// JPEG XL
jxlEffort: 'jxlef',
jxlDecodingSpeed: 'jxlds',

// Cache
refresh: 'rf',
version: 'v',

// Text / Watermark
text: 't',
textColor: 'tc',
textSize: 'ts',
textBackground: 'tbg',

// Image Processing
background: 'bg',
strip: 'st',
autoOrient: 'ao',
resize: 'rz',
mozjpeg: 'moz',
unsharp: 'unsh',
sharpen: 'sh',
blur: 'blr',
filter: 'f',
scale: 'sc',
samplingFactor: 'sf',
preserveAspectRatio: 'par',
preserveNaturalSize: 'pns',

// Advanced
faceCrop: 'fc',
faceCropPosition: 'fcp',
faceBlur: 'fb',
smartCrop: 'smc',
colorspace: 'clsp',
monochrome: 'mnchr',

// PDF
pdfPage: 'pdfp',
density: 'dnst',

// Video
videoTime: 'tm',

// Extract
extract: 'e',
extractTopX: 'p1x',
extractTopY: 'p1y',
extractBottomX: 'p2x',
extractBottomY: 'p2y',

// Other
extent: 'ett',
gifFrame: 'gf',
},
valueMap: {
// Booleans become 0 / 1
crop: Number,
webpLossless: Number,
refresh: Number,
autoOrient: Number,
resize: Number,
scale: Number,
faceCrop: Number,
faceBlur: Number,
smartCrop: Number,
monochrome: Number,
extract: Number,
// Inverted-defaults (strip/mozjpeg/par/pns default ON on the server;
// we only emit them when explicitly set to false — see getImage pre-processing)
strip: Number,
mozjpeg: Number,
preserveAspectRatio: Number,
preserveNaturalSize: Number,
// Encode colours so # does not break the URL path segment
background: (value: string) => value.startsWith('#') ? value.replace('#', '%23') : value,
textColor: (value: string) => value.startsWith('#') ? value.replace('#', '%23') : value,
textBackground: (value: string) => value.startsWith('#') ? value.replace('#', '%23') : value,
// Encode text watermarks
text: (value: string) => encodeURIComponent(value),
},
joinWith: ',',
formatter: (key, value) => `${key}_${value}`,
})

interface FlyimgOptions {
/**
* Base URL of the Flyimg server.
*
* For the official Flyimg SaaS each instance gets a unique subdomain:
* `https://img-abc123.flyimg.io`
*
* For self-hosted instances use the URL of your deployment,
* e.g. `https://images.example.com`.
*/
baseURL: string

/**
* Public base URL of your website.
*
* Only applied to **relative** image paths (e.g. `/images/photo.jpg`) —
* the value is prepended to produce an absolute URL that Flyimg can fetch.
* Absolute `src` values (e.g. from a CDN) are passed through unchanged and
* this option has no effect for those.
*
* Example: `https://www.example.com`
*/
sourceURL?: string

/**
* Flyimg process type.
*
* - `upload` (default): fetch, transform, cache and serve the image.
* - `path`: same as upload but returns the path to the cached image as a
* plain-text response body instead of serving the image directly.
*
* @default 'upload'
*/
processType?: 'upload' | 'path'
}

export default defineProvider<FlyimgOptions>({
getImage: (src, options) => {
const {
modifiers: rawModifiers = {},
baseURL,
sourceURL,
processType = 'upload',
} = options

if (import.meta.dev && !baseURL) {
console.warn('[nuxt] [image] [flyimg] `baseURL` is required. Set it in your nuxt.config under `image.flyimg.baseURL`.')
}

// --- fit → Flyimg flags ------------------------------------------------
const {
fit,
strip,
mozjpeg,
preserveAspectRatio,
preserveNaturalSize,
...rest
} = rawModifiers as Record<string, unknown>

const modifiers: Partial<Record<string, string | number | boolean>> = { ...rest } as Partial<Record<string, string | number | boolean>>

switch (fit) {
case 'cover':
// Crop to fill the target rectangle (Flyimg: c_1)
if (!modifiers.crop) modifiers.crop = true
break
case 'fill':
// Stretch to fill — disable aspect-ratio preservation (Flyimg: par_0)
if (preserveAspectRatio !== false) modifiers.preserveAspectRatio = false
break
case 'contain':
case 'inside':
// Flyimg default behaviour when width + height are given — no extra flags needed
break
case 'outside':
if (import.meta.dev) {
console.warn('[nuxt] [image] [flyimg] fit="outside" is not supported by Flyimg and will be ignored.')
}
break
}

// --- Inverted-defaults --------------------------------------------------
// strip / mozjpeg / preserveAspectRatio / preserveNaturalSize default to
// 1 (enabled) in Flyimg, so we only need to emit them when explicitly
// disabled. Treat boolean false, numeric 0, and string '0' as opt-out.
const isDisabled = (v: unknown) => v === false || v === 0 || v === '0'
if (strip != null && isDisabled(strip)) modifiers.strip = false
if (mozjpeg != null && isDisabled(mozjpeg)) modifiers.mozjpeg = false
if (preserveAspectRatio != null && isDisabled(preserveAspectRatio)) modifiers.preserveAspectRatio = false
if (preserveNaturalSize != null && isDisabled(preserveNaturalSize)) modifiers.preserveNaturalSize = false

// --- Resolve image URL -------------------------------------------------
// Flyimg needs an absolute source URL. If src is relative and sourceURL is
// configured, make it absolute.
if (import.meta.dev && !hasProtocol(src) && !sourceURL) {
console.warn('[nuxt] [image] [flyimg] `src` is a relative path but `sourceURL` is not configured. Flyimg requires an absolute source URL. Set `image.flyimg.sourceURL` in your nuxt.config.')
}
const imageUrl = !hasProtocol(src) && sourceURL
? joinURL(sourceURL, src)
: src

// --- Build Flyimg URL --------------------------------------------------
const operations = operationsGenerator(modifiers as Partial<Record<string, string | number>>)
const imageOptions = operations || '-'

// Construct the path manually so that an absolute imageUrl is treated as a
// literal path segment rather than a new base by ufo's joinURL.
// Note: if reverse-proxied via nginx, set `merge_slashes off` to prevent
// nginx from collapsing the `https://` embedded in this path. See docs.
return {
url: joinURL(baseURL || '/', `${processType}/${imageOptions}/${imageUrl}`),
}
Comment on lines +217 to +227
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Flyimg URL format image_options dash parameter support

💡 Result:

Flyimg’s URL format is:

https://<server>/<process-type>/<image_options>/<path_to_image> (e.g. /upload/w_500/...). Image options are comma-separated transformations, and each option is encoded as <key>_<value> (e.g. w_300,h_200,q_85,o_webp). A dash (-) is not a supported separator/“parameter” format for image_options—use underscores between key/value and commas between options. [1], [2]

Sources
[1] Flyimg docs – “URL Options” https://docs.flyimg.io/url-options/
[2] Klutch.sh guide – “Deploying Flyimg” https://docs.klutch.sh/guides/open-source-software/flyimg/


🌐 Web query:

Flyimg upload dash empty options segment https://demo.flyimg.io

💡 Result:

Flyimg’s upload endpoint requires a non-empty “image options” path segment:

https://demo.flyimg.io/upload/{image_options}/{source_image_url} [1][2]

So a URL like:

https://demo.flyimg.io/upload//https://example.com/img.jpg

fails because the {image_options} segment is empty.

Use at least one option, e.g.:

  • https://demo.flyimg.io/upload/w_800/https://flyimg.io/demo-images/Citroen-DS.jpg [1][2]
  • https://demo.flyimg.io/upload/q_90/https://flyimg.io/demo-images/Citroen-DS.jpg [2]

If you tell me the exact URL you tried (and the exact error text), I can point out the precise fix.


🌐 Web query:

Flyimg option parser behavior underscore key_value parameters

💡 Result:

In Flyimg, the URL “image options” segment is parsed as a list of option tokens (transformations/settings):

  • Multiple options are separated by options_separator (default comma ,, configurable in config/parameters.yml). (docs.flyimg.io)
  • Each option token is written as <key>_<value> (underscore-delimited “key_value”), e.g. w_250,q_50 or c_1. (docs.flyimg.io)
  • Many boolean-ish options are expressed by passing ..._1 (e.g. c_1, rf_1). (docs.flyimg.io)

Practical implication: _ is the key/value delimiter inside each token, while options_separator only controls how tokens are split from each other. (docs.flyimg.io)

Citations:


Replace the fallback - with a valid default option or refactor to handle no-modifier requests differently.

The code at line 219 uses operations || '-' to provide a fallback when no modifiers are provided. However, Flyimg's URL structure requires the image_options segment to contain valid key_value pairs separated by commas (e.g., w_500,q_90). A bare dash has no underscore and is not a recognized option format in Flyimg—it will not be parsed correctly and will produce invalid requests.

Additionally, Flyimg's documentation and tests confirm that a completely empty image_options segment fails (e.g., /upload//https://...). When no modifiers are provided, the code should either:

  1. Provide a sensible default option (e.g., q_75 for quality, or w_400 for a standard width).
  2. Document what behavior Flyimg exhibits with this malformed option and why it is acceptable.
  3. Refactor to omit the options segment entirely if Flyimg supports a direct format like /upload/https://... (verify with Flyimg maintainers if uncertain).

Without a fix, requests without modifiers will generate invalid Flyimg URLs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/providers/flyimg.ts` around lines 217 - 227, The current fallback
imageOptions = operations || '-' produces an invalid Flyimg segment; change the
fallback to a valid default option (e.g., replace '-' with a sensible option
string like 'q_75' or 'w_400,q_75') or refactor so when
operationsGenerator(modifiers) returns falsy you omit the options segment
entirely before calling joinURL; update the code around operationsGenerator,
imageOptions, and the joinURL call that builds the return url (using
processType, imageUrl, baseURL) so the produced path is a valid Flyimg options
segment (or excludes it) instead of a lone dash.

},
})
12 changes: 12 additions & 0 deletions test/e2e/__snapshots__/flyimg.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"requests": [
"https://demo.flyimg.io/upload/w_200/https://picsum.photos/seed/nuxtimage/800/600",
"https://demo.flyimg.io/upload/w_300,h_200,q_75,c_1/https://picsum.photos/seed/nuxtimage/800/600",
"https://demo.flyimg.io/upload/w_500,h_375,o_webp,q_80/https://picsum.photos/seed/nuxtimage/800/600",
],
"sources": [
"https://demo.flyimg.io/upload/w_500,h_375,o_webp,q_80/https://picsum.photos/seed/nuxtimage/800/600",
"https://demo.flyimg.io/upload/w_300,h_200,q_75,c_1/https://picsum.photos/seed/nuxtimage/800/600",
"https://demo.flyimg.io/upload/w_200/https://picsum.photos/seed/nuxtimage/800/600",
],
}
Loading
Loading