From 3ad71ef80dd13a90a1ecf4ac23adc580db3fac6f Mon Sep 17 00:00:00 2001 From: wotan-allfather Date: Wed, 4 Feb 2026 15:47:07 +0000 Subject: [PATCH 1/4] feat(provider): add Lorem Picsum placeholder image provider Add support for Lorem Picsum (https://picsum.photos/) as a built-in provider for placeholder images during development. Features: - Support for random images with dimensions: /200/300 - Support for specific images by ID: /id/237/200/300 - Support for seeded images: /seed/picsum/200/300 - Modifiers: grayscale, blur (1-10) Usage: ```vue ``` Closes #361 --- playground/app/providers.ts | 32 ++++++++++++++ playground/nuxt.config.ts | 1 + src/provider.ts | 1 + src/runtime/providers/picsum.ts | 74 +++++++++++++++++++++++++++++++++ test/providers.ts | 6 +++ 5 files changed, 114 insertions(+) create mode 100644 src/runtime/providers/picsum.ts diff --git a/playground/app/providers.ts b/playground/app/providers.ts index 6f26f6e93..cb84bb37a 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -435,6 +435,38 @@ export const providers: Provider[] = [ }, ], }, + // Picsum (Lorem Picsum placeholder images) + { + name: 'picsum', + samples: [ + { + src: 'id/237', + width: 200, + height: 300, + }, + { + src: 'id/870', + width: 300, + height: 200, + modifiers: { + grayscale: true, + }, + }, + { + src: 'id/1025', + width: 250, + height: 250, + modifiers: { + blur: 5, + }, + }, + { + src: 'seed/picsum', + width: 200, + height: 200, + }, + ], + }, // Prepr { name: 'prepr', diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index a5e4767f5..39fd2b216 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -91,6 +91,7 @@ export default defineNuxtConfig({ baseURL: 'https://netlify-photo-gallery.netlify.app', }, prismic: {}, + picsum: {}, prepr: { projectName: 'nuxt-prepr-demo', }, diff --git a/src/provider.ts b/src/provider.ts index 6de7fe553..d3abc643a 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -36,6 +36,7 @@ export const BuiltInProviders = [ 'netlify', 'netlifyLargeMedia', 'netlifyImageCdn', + 'picsum', 'prepr', 'none', 'prismic', diff --git a/src/runtime/providers/picsum.ts b/src/runtime/providers/picsum.ts new file mode 100644 index 000000000..9b1869d11 --- /dev/null +++ b/src/runtime/providers/picsum.ts @@ -0,0 +1,74 @@ +// https://picsum.photos/ - Lorem Picsum placeholder images + +import { joinURL, withQuery } from 'ufo' +import { defineProvider } from '../utils/provider' + +interface PicsumModifiers { + grayscale?: boolean + blur?: number +} + +interface PicsumOptions { + baseURL?: string + modifiers?: PicsumModifiers +} + +export const picsumCDN = 'https://picsum.photos/' + +export default defineProvider({ + getImage: (src, { modifiers, baseURL = picsumCDN }) => { + const { width, height, grayscale, blur, ...otherModifiers } = modifiers || {} + + // Build the path + // Picsum URL format: https://picsum.photos/[id/{id}/]{width}[/{height}] + // Examples: + // - Random: https://picsum.photos/200/300 + // - Specific ID: https://picsum.photos/id/237/200/300 + // - Square: https://picsum.photos/200 + + const parts: string[] = [] + + // If src is provided and not empty, it could be: + // - "id/237" for a specific image + // - "seed/picsum" for a seeded image + if (src && src !== '/') { + const cleanSrc = src.startsWith('/') ? src.slice(1) : src + if (cleanSrc) { + parts.push(cleanSrc) + } + } + + // Add dimensions - these come after the ID/seed path + if (width) { + parts.push(String(width)) + } + if (height) { + parts.push(String(height)) + } + + // Build query parameters for modifiers + const query: Record = {} + + if (grayscale) { + query.grayscale = '' + } + + if (blur !== undefined && blur > 0) { + // Picsum blur accepts values from 1-10 + query.blur = Math.min(Math.max(Math.round(blur), 1), 10) + } + + // Add any other custom modifiers (excluding standard ones that don't apply to picsum) + for (const [key, value] of Object.entries(otherModifiers)) { + if (value !== undefined && value !== null && !['fit', 'format', 'quality', 'background'].includes(key)) { + query[key] = value as string | number + } + } + + const url = joinURL(baseURL, ...parts) + + return { + url: Object.keys(query).length > 0 ? withQuery(url, query) : url, + } + }, +}) diff --git a/test/providers.ts b/test/providers.ts index 01b662bd7..e9b24539d 100644 --- a/test/providers.ts +++ b/test/providers.ts @@ -37,6 +37,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net' }, + picsum: { url: 'https://picsum.photos/test.png' }, }, { args: ['/test.png', { width: 200 }], @@ -76,6 +77,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200' }, + picsum: { url: 'https://picsum.photos/test.png/200' }, }, { args: ['/test.png', { height: 200 }], @@ -114,6 +116,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=height:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?height=200' }, + picsum: { url: 'https://picsum.photos/test.png/200' }, }, { args: ['/test.png', { width: 200, height: 200 }], @@ -152,6 +155,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, + picsum: { url: 'https://picsum.photos/test.png/200/200' }, }, { args: ['/test.png', { width: 200, height: 200, fit: 'contain' }], @@ -190,6 +194,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, + picsum: { url: 'https://picsum.photos/test.png/200/200' }, }, { args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }], @@ -228,5 +233,6 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/output=format:jpeg/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, + picsum: { url: 'https://picsum.photos/test.png/200/200' }, }, ] as const From 0140c9fb4b4d5aa893280a6533042c627aa1a2b9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 7 Feb 2026 22:23:51 +0000 Subject: [PATCH 2/4] docs: add documentation --- docs/content/3.providers/picsum.md | 102 +++++++++++++++++++++++++++++ docs/public/providers/picsum.svg | 1 + 2 files changed, 103 insertions(+) create mode 100644 docs/content/3.providers/picsum.md create mode 100644 docs/public/providers/picsum.svg diff --git a/docs/content/3.providers/picsum.md b/docs/content/3.providers/picsum.md new file mode 100644 index 000000000..0a4e50827 --- /dev/null +++ b/docs/content/3.providers/picsum.md @@ -0,0 +1,102 @@ +--- +title: Picsum +description: Nuxt Image with Lorem Picsum placeholder images. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/image/blob/main/src/runtime/providers/picsum.ts + size: xs +--- + +Integration between [Lorem Picsum](https://picsum.photos/) and the image module. + +Lorem Picsum provides random placeholder images. It is useful during development or for demo purposes. + +To use this provider you just need to enable it in your Nuxt config: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + image: { + picsum: {} + } +}) +``` + +## Usage + +### Random image + +Get a random image with specific dimensions: + +```vue + +``` + +### Specific image by ID + +Use `id/{id}` as the `src` to get a specific image: + +```vue + +``` + +### Seeded random image + +Use `seed/{seed}` as the `src` to get a consistent random image based on a seed string: + +```vue + +``` + +## Modifiers + +### `grayscale` + +Return the image in grayscale. + +```vue + +``` + +### `blur` + +Apply a blur effect to the image. Accepts values from 1 to 10. + +```vue + +``` + +::note +The `src` value must be either `id/{id}` for a specific image, `seed/{seed}` for a seeded random image, or `/` for a fully random image. Other `src` values are ignored and a random image will be returned. +:: + +::note +Picsum does not support height-only resizing. When only `height` is provided without `width`, a square image of that dimension will be returned. +:: diff --git a/docs/public/providers/picsum.svg b/docs/public/providers/picsum.svg new file mode 100644 index 000000000..d40078ed9 --- /dev/null +++ b/docs/public/providers/picsum.svg @@ -0,0 +1 @@ + From 17bc9a8e9553e22daf45316b647f2f80f040b6c0 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 7 Feb 2026 22:24:00 +0000 Subject: [PATCH 3/4] fix: improve validation --- src/runtime/providers/picsum.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/providers/picsum.ts b/src/runtime/providers/picsum.ts index 9b1869d11..a3d34cbed 100644 --- a/src/runtime/providers/picsum.ts +++ b/src/runtime/providers/picsum.ts @@ -32,9 +32,9 @@ export default defineProvider({ // - "id/237" for a specific image // - "seed/picsum" for a seeded image if (src && src !== '/') { - const cleanSrc = src.startsWith('/') ? src.slice(1) : src - if (cleanSrc) { - parts.push(cleanSrc) + const [type, id] = (src.startsWith('/') ? src.slice(1) : src).split('/') + if (type && (type === 'id' || type === 'seed')) { + parts.push(`${type}/${id}`) } } From 33ee77c8c8485a0fbf8f5739c0da22dd8a8d9457 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 7 Feb 2026 22:27:52 +0000 Subject: [PATCH 4/4] test: update and run tests --- test/e2e/__snapshots__/picsum.json5 | 14 ++++++++++++++ test/nuxt/providers.test.ts | 11 +++++++++++ test/providers.ts | 12 ++++++------ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 test/e2e/__snapshots__/picsum.json5 diff --git a/test/e2e/__snapshots__/picsum.json5 b/test/e2e/__snapshots__/picsum.json5 new file mode 100644 index 000000000..e0ede4c65 --- /dev/null +++ b/test/e2e/__snapshots__/picsum.json5 @@ -0,0 +1,14 @@ +{ + "requests": [ + "https://picsum.photos/id/1025/250/250?blur=5", + "https://picsum.photos/id/237/200/300", + "https://picsum.photos/id/870/300/200?grayscale", + "https://picsum.photos/seed/picsum/200/200", + ], + "sources": [ + "https://picsum.photos/id/237/200/300", + "https://picsum.photos/id/870/300/200?grayscale", + "https://picsum.photos/id/1025/250/250?blur=5", + "https://picsum.photos/seed/picsum/200/200", + ], +} \ No newline at end of file diff --git a/test/nuxt/providers.test.ts b/test/nuxt/providers.test.ts index 2fde75a84..12671e082 100644 --- a/test/nuxt/providers.test.ts +++ b/test/nuxt/providers.test.ts @@ -12,6 +12,7 @@ import cloudflare from '../../dist/runtime/providers/cloudflare' import cloudinary from '../../dist/runtime/providers/cloudinary' import twicpics from '../../dist/runtime/providers/twicpics' import fastly from '../../dist/runtime/providers/fastly' +import picsum from '../../dist/runtime/providers/picsum' import prepr from '../../dist/runtime/providers/prepr' import glide from '../../dist/runtime/providers/glide' import imgix from '../../dist/runtime/providers/imgix' @@ -462,6 +463,16 @@ describe('Providers', () => { expect(generated).toMatchObject(image.prepr) } }) + it('picsum', () => { + const providerOptions = {} + + for (const image of images) { + const [_src, modifiers] = image.args + const generated = picsum().getImage('', { modifiers: { ...modifiers }, ...providerOptions }, emptyContext) + expect(generated).toMatchObject(image.picsum) + } + }) + it('contentful', () => { const providerOptions = { baseURL: '', diff --git a/test/providers.ts b/test/providers.ts index e9b24539d..f25b56fc2 100644 --- a/test/providers.ts +++ b/test/providers.ts @@ -37,7 +37,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net' }, - picsum: { url: 'https://picsum.photos/test.png' }, + picsum: { url: 'https://picsum.photos/' }, }, { args: ['/test.png', { width: 200 }], @@ -77,7 +77,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200' }, - picsum: { url: 'https://picsum.photos/test.png/200' }, + picsum: { url: 'https://picsum.photos/200' }, }, { args: ['/test.png', { height: 200 }], @@ -116,7 +116,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=height:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?height=200' }, - picsum: { url: 'https://picsum.photos/test.png/200' }, + picsum: { url: 'https://picsum.photos/200' }, }, { args: ['/test.png', { width: 200, height: 200 }], @@ -155,7 +155,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, - picsum: { url: 'https://picsum.photos/test.png/200/200' }, + picsum: { url: 'https://picsum.photos/200/200' }, }, { args: ['/test.png', { width: 200, height: 200, fit: 'contain' }], @@ -194,7 +194,7 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/auto_image/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, - picsum: { url: 'https://picsum.photos/test.png/200/200' }, + picsum: { url: 'https://picsum.photos/200/200' }, }, { args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }], @@ -233,6 +233,6 @@ export const images = [ hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/output=format:jpeg/cltsrex89477t08unlckqx9ue' }, caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' }, bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' }, - picsum: { url: 'https://picsum.photos/test.png/200/200' }, + picsum: { url: 'https://picsum.photos/200/200' }, }, ] as const