-
Notifications
You must be signed in to change notification settings - Fork 319
feat(flyimg): add Flyimg built-in provider #2125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bernhardberger
wants to merge
1
commit into
nuxt:main
Choose a base branch
from
bernhardberger:feat/flyimg-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| ```` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
|
|
||
| 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}`), | ||
| } | ||
| }, | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ], | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 forimage_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
uploadendpoint 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.jpgfails 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):
options_separator(default comma,, configurable inconfig/parameters.yml). (docs.flyimg.io)<key>_<value>(underscore-delimited “key_value”), e.g.w_250,q_50orc_1. (docs.flyimg.io)..._1(e.g.c_1,rf_1). (docs.flyimg.io)Practical implication:
_is the key/value delimiter inside each token, whileoptions_separatoronly 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 theimage_optionssegment to contain validkey_valuepairs 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_optionssegment fails (e.g.,/upload//https://...). When no modifiers are provided, the code should either:q_75for quality, orw_400for a standard width)./upload/https://...(verify with Flyimg maintainers if uncertain).Without a fix, requests without modifiers will generate invalid Flyimg URLs.
🤖 Prompt for AI Agents