Repository: nuxt/image
Pull request #1019 (2023-09-30 11:09:45)

feat: add `weserv` provider
Most options are in the keymap, the documentation is just a link to the website. That could be improved but frankly that's a lot of work. Maybe someone can put ai to use and summaries the doc for the provider documentation

Recreated as per: https://github.com/nuxt/image/pull/962#issuecomment-1740714317

nuxt-studio[bot]: 
### <span aria-hidden="true">✅</span> Live Preview ready!

| Name | Edit | Preview | Latest Commit |
| :--- | :----- | :------ | :------ |
| **Image** | [Edit on Studio ↗︎](https://nuxt.studio/@nuxt/image/studio/content?refId=%231019) | [View Live Preview](https://image.nuxt.com?preview=92e50220cf84e44dcf68ea4bada77c5a) | 305b1b8de9375dda7e0aabcc3a5f9ea928601607 |

<!-- This comment is generated by [Nuxt Studio](https://nuxt.studio) -->
<!-- Do not remove or edit this comment. -->

nandi95: > Would you be able to add a few test cases for `weserv` in `playground/providers.ts`, `test/unit/providers.test.ts` and `test/providers.ts`? 🙏

<img width="1371" alt="Screenshot 2023-10-02 at 23 02 05" src="https://github.com/nuxt/image/assets/41805560/ea1fdc6c-753a-4f9d-98ba-19d31f929466">


I can't seem to run the tests. I installed with pnpm and run the test with it but I get the above.
danielroe: You can try running `pnpm dev:prepare` first.
nandi95: I know the correct url is generated, and when testing it by pasting it into the browser, I get the result I expected yet. The ssr and no-ssr tests are not working. Can you perhaps help me on this front?
kleisauke: As per the request made in https://github.com/nuxt/image/pull/962#issuecomment-1706726140, I'd like to provide my feedback:
- Please consider replacing `weserve` with `weserv`.
- Currently, `https://wsrv.nl` is hard-coded, and there's no flexibility to modify it. It's worth noting that many websites host their own solutions using our open-source code.
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) Report
Attention: `12 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`e4572e7`)](https://app.codecov.io/gh/nuxt/image/commit/e4572e77dbb9f15ad955d42f06e9e747d979b984?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) 89.98% compared to head [(`18d16ec`)](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) 89.95%.

| [Files](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) | Patch % | Lines |
|---|---|---|
| [src/runtime/providers/weserv.ts](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt#diff-c3JjL3J1bnRpbWUvcHJvdmlkZXJzL3dlc2Vydi50cw==) | 89.83% | [12 Missing :warning: ](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1019      +/-   ##
==========================================
- Coverage   89.98%   89.95%   -0.04%     
==========================================
  Files          47       48       +1     
  Lines        3156     3275     +119     
  Branches      359      366       +7     
==========================================
+ Hits         2840     2946     +106     
- Misses        315      327      +12     
- Partials        1        2       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nuxt/image/pull/1019?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt).


Patch:
diff --git a/docs/content/3.providers/weserv.md b/docs/content/3.providers/weserv.md
new file mode 100644
index 0000000..8159305
--- /dev/null
+++ b/docs/content/3.providers/weserv.md
@@ -0,0 +1,11 @@
+---
+title: Weserv
+description: Optimize images with Weserv's dynamic image transformation service.
+---
+
+Weserv offers a wide range of image transformation for all JPEG, PNG, and GIF files.
+
+
+## Modifiers
+
+In addition to `height` and `width`, the Weserv provider supports numerous modifiers documented [here](https://images.weserv.nl/docs/quick-reference.html).
diff --git a/docs/package.json b/docs/package.json
index 5d376df..82d35c5 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -10,7 +10,7 @@
   },
   "dependencies": {
     "@iconify-json/ph": "^1.1.9",
-    "@iconify-json/simple-icons": "^1.1.84",
+    "@iconify-json/simple-icons": "^1.1.85",
     "@nuxt/content": "^2.9.0",
     "@nuxt/image": "link:..",
     "@nuxt/ui-pro": "^0.5.0",
@@ -27,7 +27,7 @@
   "devDependencies": {
     "@antfu/eslint-config": "^2.4.6",
     "@nuxt/devtools": "^1.0.6",
-    "@types/node": "^20.10.4",
+    "@types/node": "^20.10.5",
     "eslint": "^8.56.0"
   }
 }
diff --git a/docs/pages/index.vue b/docs/pages/index.vue
index 1842580..6b4c877 100644
--- a/docs/pages/index.vue
+++ b/docs/pages/index.vue
@@ -7,7 +7,7 @@ const description = 'Plug-and-play image optimization for Nuxt apps. Resize and
 useSeoMeta({
   titleTemplate: '',
   title,
-  ogTitle: title, 
+  ogTitle: title,
   description,
   ogDescription: description,
   ogImage: 'https://image.nuxt.com/social-card.png',
@@ -16,7 +16,7 @@ useSeoMeta({
 const source = ref('npm i @nuxt/image')
 const { copy, copied } = useClipboard({ source })
 
-const providers = ['cloudflare', 'cloudimage', 'cloudinary', 'directus', 'edgio', 'fastly', 'glide', 'gumlet', 'hygraph', 'imageengine', 'imagekit', 'imgix', 'ipx', 'netlify', 'prepr', 'prismic', 'sanity', 'storyblok', 'strapi', 'twicpics', 'unsplash', 'uploadcare', 'vercel']
+const providers = ['cloudflare', 'cloudimage', 'cloudinary', 'directus', 'edgio', 'fastly', 'glide', 'gumlet', 'hygraph', 'imageengine', 'imagekit', 'imgix', 'ipx', 'netlify', 'prepr', 'prismic', 'sanity', 'storyblok', 'strapi', 'twicpics', 'unsplash', 'uploadcare', 'vercel', 'weserv']
 // Disabling because svg to png does not work now with SSG
 // Related issue: https://github.com/unjs/ipx/issues/160
 // const img = useImage()
diff --git a/docs/public/providers/weserv.svg b/docs/public/providers/weserv.svg
new file mode 100644
index 0000000..0d63a54
--- /dev/null
+++ b/docs/public/providers/weserv.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
+  <defs>
+    <g id="template">
+      <rect stroke-linejoin="round" fill="none" x="0.5" y="0.5" width="13" height="10"/>
+      <rect fill="#fff" stroke="none" opacity="0.2" x="1" y="1" width="12" height="9"/>
+      <line stroke-linecap="square" x1="1" y1="8.15" x2="4.5" y2="4.64"/>
+      <line stroke-linecap="square" x1="4.5" y1="4.64" x2="9.8" y2="10"/>
+      <line stroke-linecap="square" x1="7.73" y1="7.91" x2="10" y2="5.64"/>
+      <line stroke-linecap="square" x1="10" y1="5.64" x2="13" y2="8.66"/>
+      <circle stroke="none" cx="11" cy="3" r="1"/>
+    </g>
+  </defs>
+  <use id="base" fill="#A72376" stroke="#A72376" xlink:href="#template"/>
+  <use id="scaled" fill="#000" stroke="#000" x="5.9" y="8.9" transform="scale(0.8)" xlink:href="#template"/>
+</svg>
diff --git a/package.json b/package.json
index f660740..f921cd3 100755
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
     "@nuxt/module-builder": "0.5.4",
     "@nuxt/test-utils": "^3.9.0-alpha.1",
     "@nuxtjs/eslint-config-typescript": "^12.1.0",
-    "@types/node": "^20.10.4",
+    "@types/node": "^20.10.5",
     "@vitest/coverage-v8": "^1.0.4",
     "@vue/test-utils": "^2.4.3",
     "changelogen": "^0.5.5",
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index 59653ef..40af785 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -104,6 +104,9 @@ export default defineNuxtConfig({
     hygraph: {
       baseURL: 'https://media.graphassets.com'
     },
+    weserv: {
+      baseURL: '/'
+    },
     providers: {
       custom: {
         provider: '~/providers/custom',
diff --git a/playground/providers.ts b/playground/providers.ts
index eeabc2a..f4a9082 100644
--- a/playground/providers.ts
+++ b/playground/providers.ts
@@ -951,6 +951,31 @@ export const providers: Provider[] = [
       }
     ]
   },
+  // weserv
+  {
+    name: 'weserv',
+    samples: [
+      {
+        src: 'https://picsum.photos/200',
+        format: 'webp'
+      },
+      {
+        src: 'https://picsum.photos/200',
+        format: 'webp',
+        modifiers: { mask: 'circle' }
+      },
+      {
+        src: 'https://picsum.photos/200',
+        format: 'webp',
+        modifiers: { mask: 'circle', filter: 'sepia' }
+      },
+      {
+        src: 'https://picsum.photos/200',
+        format: 'webp',
+        modifiers: { mask: 'circle', filter: 'sepia', quality: 80 }
+      }
+    ]
+  },
   // Sirv
   {
     name: 'sirv',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 572308e..b1c4fbc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -61,8 +61,8 @@ importers:
         specifier: ^12.1.0
         version: 12.1.0(eslint@8.56.0)(typescript@5.3.3)
       '@types/node':
-        specifier: ^20.10.4
-        version: 20.10.4
+        specifier: ^20.10.5
+        version: 20.10.5
       '@vitest/coverage-v8':
         specifier: ^1.0.4
         version: 1.0.4(vitest@1.0.4)
@@ -86,7 +86,7 @@ importers:
         version: 1.21.0
       nuxt:
         specifier: ^3.8.2
-        version: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+        version: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
       playwright-core:
         specifier: ^1.40.1
         version: 1.40.1
@@ -95,7 +95,7 @@ importers:
         version: 5.3.3
       vitest:
         specifier: ^1.0.4
-        version: 1.0.4(@types/node@20.10.4)(happy-dom@12.10.3)
+        version: 1.0.4(@types/node@20.10.5)(happy-dom@12.10.3)
       vitest-environment-nuxt:
         specifier: ^1.0.0-alpha.1
         version: 1.0.0-alpha.1(@vue/test-utils@2.4.3)(h3@1.9.0)(happy-dom@12.10.3)(playwright-core@1.40.1)(rollup@3.29.4)(vite@5.0.7)(vitest@1.0.4)(vue-router@4.2.5)(vue@3.3.8)
@@ -109,8 +109,8 @@ importers:
         specifier: ^1.1.9
         version: 1.1.9
       '@iconify-json/simple-icons':
-        specifier: ^1.1.84
-        version: 1.1.84
+        specifier: ^1.1.85
+        version: 1.1.85
       '@nuxt/content':
         specifier: ^2.9.0
         version: 2.9.0(nuxt@3.8.2)(rollup@3.29.4)(vue@3.3.8)
@@ -140,7 +140,7 @@ importers:
         version: 10.7.0(nuxt@3.8.2)(rollup@3.29.4)(vue@3.3.8)
       nuxt:
         specifier: ^3.8.2
-        version: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+        version: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
       nuxt-og-image:
         specifier: ^2.2.4
         version: 2.2.4(rollup@3.29.4)(vue@3.3.8)
@@ -155,8 +155,8 @@ importers:
         specifier: ^1.0.6
         version: 1.0.6(nuxt@3.8.2)(rollup@3.29.4)(vite@5.0.7)
       '@types/node':
-        specifier: ^20.10.4
-        version: 20.10.4
+        specifier: ^20.10.5
+        version: 20.10.5
       eslint:
         specifier: ^8.56.0
         version: 8.56.0
@@ -168,7 +168,7 @@ importers:
         version: link:..
       nuxt:
         specifier: latest
-        version: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+        version: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
 
   playground:
     devDependencies:
@@ -177,7 +177,7 @@ importers:
         version: link:..
       nuxt:
         specifier: latest
-        version: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+        version: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
 
 packages:
 
@@ -1123,8 +1123,8 @@ packages:
       '@iconify/types': 2.0.0
     dev: false
 
-  /@iconify-json/simple-icons@1.1.84:
-    resolution: {integrity: sha512-Mg7XHuRaXgZKlr+bz+vjuwy4atREUuHxwkQWymNnmL8cP1g0LI5Euh0bEYIcfmgMi8UiJQ+ALVM7ZSoAEP2uHQ==}
+  /@iconify-json/simple-icons@1.1.85:
+    resolution: {integrity: sha512-INgYqgh+zZZgqjD22HVSFYy7m0ben88lG4JuS41emxf23BVJcvDidADAh78H/+5U0vdc2zfceglVR893VS+TaQ==}
     dependencies:
       '@iconify/types': 2.0.0
     dev: false
@@ -1422,8 +1422,8 @@ packages:
       '@nuxt/kit': 3.8.2(rollup@3.29.4)
       '@nuxt/schema': 3.8.2(rollup@3.29.4)
       execa: 7.2.0
-      nuxt: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
-      vite: 5.0.7(@types/node@20.10.4)
+      nuxt: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+      vite: 5.0.7(@types/node@20.10.5)
     transitivePeerDependencies:
       - rollup
       - supports-color
@@ -1437,8 +1437,8 @@ packages:
       '@nuxt/kit': 3.8.2(rollup@3.29.4)
       '@nuxt/schema': 3.8.2(rollup@3.29.4)
       execa: 7.2.0
-      nuxt: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
-      vite: 5.0.7(@types/node@20.10.4)
+      nuxt: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+      vite: 5.0.7(@types/node@20.10.5)
     transitivePeerDependencies:
       - rollup
       - supports-color
@@ -1502,7 +1502,7 @@ packages:
       local-pkg: 0.5.0
       magicast: 0.3.2
       nitropack: 2.8.1
-      nuxt: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+      nuxt: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
       nypm: 0.3.3
       ofetch: 1.3.3
       ohash: 1.1.3
@@ -1516,7 +1516,7 @@ packages:
       simple-git: 3.21.0
       sirv: 2.0.3
       unimport: 3.6.1(rollup@3.29.4)
-      vite: 5.0.7(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
       vite-plugin-inspect: 0.8.1(@nuxt/kit@3.8.2)(rollup@3.29.4)(vite@5.0.7)
       vite-plugin-vue-inspector: 4.0.2(vite@5.0.7)
       which: 3.0.1
@@ -1569,7 +1569,7 @@ packages:
       local-pkg: 0.5.0
       magicast: 0.3.2
       nitropack: 2.8.1
-      nuxt: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+      nuxt: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
       nypm: 0.3.3
       ofetch: 1.3.3
       ohash: 1.1.3
@@ -1583,7 +1583,7 @@ packages:
       simple-git: 3.21.0
       sirv: 2.0.3
       unimport: 3.6.1(rollup@3.29.4)
-      vite: 5.0.7(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
       vite-plugin-inspect: 0.8.1(@nuxt/kit@3.8.2)(rollup@3.29.4)(vite@5.0.7)
       vite-plugin-vue-inspector: 4.0.2(vite@5.0.7)
       which: 3.0.1
@@ -1757,8 +1757,8 @@ packages:
       ufo: 1.3.2
       unenv: 1.8.0
       unplugin: 1.5.1
-      vite: 5.0.7(@types/node@20.10.4)
-      vitest: 1.0.4(@types/node@20.10.4)(happy-dom@12.10.3)
+      vite: 5.0.7(@types/node@20.10.5)
+      vitest: 1.0.4(@types/node@20.10.5)(happy-dom@12.10.3)
       vitest-environment-nuxt: 1.0.0-alpha.1(@vue/test-utils@2.4.3)(h3@1.9.0)(happy-dom@12.10.3)(playwright-core@1.40.1)(rollup@3.29.4)(vite@5.0.7)(vitest@1.0.4)(vue-router@4.2.5)(vue@3.3.8)
       vue: 3.3.8(typescript@5.3.3)
       vue-router: 4.2.5(vue@3.3.8)
@@ -1848,7 +1848,7 @@ packages:
       - vue
     dev: false
 
-  /@nuxt/vite-builder@3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vue-tsc@1.8.25)(vue@3.3.8):
+  /@nuxt/vite-builder@3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vue-tsc@1.8.25)(vue@3.3.8):
     resolution: {integrity: sha512-l/lzDDTbd3M89BpmWqjhVLgLVRqfkKp0tyYgV5seJQjj3SX+IeqI7k6k8+dMEifdeO34jUajVWptNpITXQryyg==}
     engines: {node: ^14.18.0 || >=16.10.0}
     peerDependencies:
@@ -1883,8 +1883,8 @@ packages:
       strip-literal: 1.3.0
       ufo: 1.3.2
       unplugin: 1.5.1
-      vite: 4.5.0(@types/node@20.10.4)
-      vite-node: 0.33.0(@types/node@20.10.4)
+      vite: 4.5.0(@types/node@20.10.5)
+      vite-node: 0.33.0(@types/node@20.10.5)
       vite-plugin-checker: 0.6.2(eslint@8.56.0)(typescript@5.3.3)(vite@4.5.0)(vue-tsc@1.8.25)
       vue: 3.3.8(typescript@5.3.3)
       vue-bundle-renderer: 2.0.0
@@ -2879,7 +2879,7 @@ packages:
     resolution: {integrity: sha512-sF59BlXtUdzEAL1u0MSvuzWd7PdZvZEtnaVkzX5mjpdWTJ8brG0jUqve3jPCzSzvAKKMHTG8F8o/WMQLtleZdQ==}
     dependencies:
       '@types/jsonfile': 6.1.2
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
     dev: false
 
   /@types/hast@2.3.6:
@@ -2897,7 +2897,7 @@ packages:
   /@types/http-proxy@1.17.14:
     resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==}
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
 
   /@types/istanbul-lib-coverage@2.0.4:
     resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
@@ -2914,7 +2914,7 @@ packages:
   /@types/jsonfile@6.1.2:
     resolution: {integrity: sha512-8t92P+oeW4d/CRQfJaSqEwXujrhH4OEeHRjGU3v1Q8mUS8GPF3yiX26sw4svv6faL2HfBtGTe2xWIoVgN3dy9w==}
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
     dev: false
 
   /@types/mdast@3.0.13:
@@ -2932,8 +2932,8 @@ packages:
     resolution: {integrity: sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==}
     dev: false
 
-  /@types/node@20.10.4:
-    resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
+  /@types/node@20.10.5:
+    resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==}
     dependencies:
       undici-types: 5.26.5
 
@@ -3292,7 +3292,7 @@ packages:
       '@babel/core': 7.23.3
       '@babel/plugin-transform-typescript': 7.23.4(@babel/core@7.23.3)
       '@vue/babel-plugin-jsx': 1.1.5(@babel/core@7.23.3)
-      vite: 4.5.0(@types/node@20.10.4)
+      vite: 4.5.0(@types/node@20.10.5)
       vue: 3.3.8(typescript@5.3.3)
     transitivePeerDependencies:
       - supports-color
@@ -3304,7 +3304,7 @@ packages:
       vite: ^4.0.0 || ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 4.5.0(@types/node@20.10.4)
+      vite: 4.5.0(@types/node@20.10.5)
       vue: 3.3.8(typescript@5.3.3)
 
   /@vitest/coverage-v8@1.0.4(vitest@1.0.4):
@@ -3325,7 +3325,7 @@ packages:
       std-env: 3.6.0
       test-exclude: 6.0.0
       v8-to-istanbul: 9.2.0
-      vitest: 1.0.4(@types/node@20.10.4)(happy-dom@12.10.3)
+      vitest: 1.0.4(@types/node@20.10.5)(happy-dom@12.10.3)
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -3684,7 +3684,7 @@ packages:
       '@vueuse/core': 10.7.0(vue@3.3.8)
       '@vueuse/metadata': 10.7.0
       local-pkg: 0.5.0
-      nuxt: 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
+      nuxt: 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25)
       vue-demi: 0.14.6(vue@3.3.8)
     transitivePeerDependencies:
       - '@vue/composition-api'
@@ -4369,7 +4369,7 @@ packages:
     engines: {node: '>=12.13.0'}
     hasBin: true
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
       escape-string-regexp: 4.0.0
       is-wsl: 2.2.0
       lighthouse-logger: 2.0.1
@@ -5828,7 +5828,7 @@ packages:
       '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3)
       '@typescript-eslint/utils': 6.13.2(eslint@8.56.0)(typescript@5.3.3)
       eslint: 8.56.0
-      vitest: 1.0.4(@types/node@20.10.4)(happy-dom@12.10.3)
+      vitest: 1.0.4(@types/node@20.10.5)(happy-dom@12.10.3)
     transitivePeerDependencies:
       - supports-color
       - typescript
@@ -9177,7 +9177,7 @@ packages:
       - vue
     dev: false
 
-  /nuxt@3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25):
+  /nuxt@3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vite@5.0.7)(vue-tsc@1.8.25):
     resolution: {integrity: sha512-HUAyifmqTs2zcQBGvcby3KNs2pBAk+l7ZbLjD1oCNqQQ+wBuZ1qgLC4Ebu++y4g3o3Y8WAWSvpafbKRLQZziPw==}
     engines: {node: ^14.18.0 || >=16.10.0}
     hasBin: true
@@ -9196,8 +9196,8 @@ packages:
       '@nuxt/schema': 3.8.2(rollup@3.29.4)
       '@nuxt/telemetry': 2.5.2(rollup@3.29.4)
       '@nuxt/ui-templates': 1.3.1
-      '@nuxt/vite-builder': 3.8.2(@types/node@20.10.4)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vue-tsc@1.8.25)(vue@3.3.8)
-      '@types/node': 20.10.4
+      '@nuxt/vite-builder': 3.8.2(@types/node@20.10.5)(eslint@8.56.0)(rollup@3.29.4)(typescript@5.3.3)(vue-tsc@1.8.25)(vue@3.3.8)
+      '@types/node': 20.10.5
       '@unhead/dom': 1.8.4
       '@unhead/ssr': 1.8.4
       '@unhead/vue': 1.8.4(vue@3.3.8)
@@ -12059,7 +12059,7 @@ packages:
       vfile-message: 4.0.2
     dev: false
 
-  /vite-node@0.33.0(@types/node@20.10.4):
+  /vite-node@0.33.0(@types/node@20.10.5):
     resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -12069,7 +12069,7 @@ packages:
       mlly: 1.4.2
       pathe: 1.1.1
       picocolors: 1.0.0
-      vite: 4.5.0(@types/node@20.10.4)
+      vite: 4.5.0(@types/node@20.10.5)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -12080,7 +12080,7 @@ packages:
       - supports-color
       - terser
 
-  /vite-node@1.0.4(@types/node@20.10.4):
+  /vite-node@1.0.4(@types/node@20.10.5):
     resolution: {integrity: sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -12089,7 +12089,7 @@ packages:
       debug: 4.3.4
       pathe: 1.1.1
       picocolors: 1.0.0
-      vite: 5.0.7(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -12147,7 +12147,7 @@ packages:
       strip-ansi: 6.0.1
       tiny-invariant: 1.3.1
       typescript: 5.3.3
-      vite: 4.5.0(@types/node@20.10.4)
+      vite: 4.5.0(@types/node@20.10.5)
       vscode-languageclient: 7.0.0
       vscode-languageserver: 7.0.0
       vscode-languageserver-textdocument: 1.0.11
@@ -12173,7 +12173,7 @@ packages:
       open: 9.1.0
       picocolors: 1.0.0
       sirv: 2.0.3
-      vite: 5.0.7(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
     transitivePeerDependencies:
       - rollup
       - supports-color
@@ -12192,11 +12192,11 @@ packages:
       '@vue/compiler-dom': 3.3.8
       kolorist: 1.8.0
       magic-string: 0.30.5
-      vite: 5.0.7(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
     transitivePeerDependencies:
       - supports-color
 
-  /vite@4.5.0(@types/node@20.10.4):
+  /vite@4.5.0(@types/node@20.10.5):
     resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
     engines: {node: ^14.18.0 || >=16.0.0}
     hasBin: true
@@ -12224,14 +12224,14 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
       esbuild: 0.18.20
       postcss: 8.4.32
       rollup: 3.29.4
     optionalDependencies:
       fsevents: 2.3.3
 
-  /vite@5.0.7(@types/node@20.10.4):
+  /vite@5.0.7(@types/node@20.10.5):
     resolution: {integrity: sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -12259,7 +12259,7 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
       esbuild: 0.19.8
       postcss: 8.4.32
       rollup: 4.7.0
@@ -12287,7 +12287,7 @@ packages:
       - vue-router
     dev: true
 
-  /vitest@1.0.4(@types/node@20.10.4)(happy-dom@12.10.3):
+  /vitest@1.0.4(@types/node@20.10.5)(happy-dom@12.10.3):
     resolution: {integrity: sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -12312,7 +12312,7 @@ packages:
       jsdom:
         optional: true
     dependencies:
-      '@types/node': 20.10.4
+      '@types/node': 20.10.5
       '@vitest/expect': 1.0.4
       '@vitest/runner': 1.0.4
       '@vitest/snapshot': 1.0.4
@@ -12332,8 +12332,8 @@ packages:
       strip-literal: 1.3.0
       tinybench: 2.5.1
       tinypool: 0.8.1
-      vite: 5.0.7(@types/node@20.10.4)
-      vite-node: 1.0.4(@types/node@20.10.4)
+      vite: 5.0.7(@types/node@20.10.5)
+      vite-node: 1.0.4(@types/node@20.10.5)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
diff --git a/src/provider.ts b/src/provider.ts
index 42782d6..3377e25 100644
--- a/src/provider.ts
+++ b/src/provider.ts
@@ -41,6 +41,7 @@ const BuiltInProviders = [
   'uploadcare',
   'vercel',
   'wagtail',
+  'weserv',
   'sirv'
 ] as const
 
diff --git a/src/runtime/providers/weserv.ts b/src/runtime/providers/weserv.ts
new file mode 100644
index 0000000..74c67a5
--- /dev/null
+++ b/src/runtime/providers/weserv.ts
@@ -0,0 +1,118 @@
+import { withBase } from 'ufo'
+import type { OperationGeneratorConfig, ProviderGetImage, ImageOptions } from '../../types'
+import { createOperationsGenerator } from '#image'
+import { createError } from '#imports'
+
+const operationsGenerator = createOperationsGenerator({
+  keyMap: {
+    format: 'output',
+    width: 'w',
+    height: 'h',
+    quality: 'q',
+    background: 'bg',
+    pixelDensity: 'dpr',
+    trimImage: 'trim',
+    sharpen: 'sharp',
+    brightness: 'mod',
+    saturation: 'sat',
+    hue: 'hue',
+    filter: 'filt',
+    gamma: 'gam',
+    contrast: 'con',
+    blur: 'blur',
+    mirror: 'flop',
+    rotate: 'rot',
+    mask: 'mask',
+    maskTrim: 'mtrim',
+    maskBackground: 'mbg'
+  },
+  valueMap: {
+    format: {
+      jpeg: 'jpg',
+      jpg: 'jpg',
+      png: 'png',
+      webp: 'webp'
+    },
+    fit: {
+      cover: 'cover',
+      contain: 'contain',
+      fill: 'fill',
+      inside: 'inside',
+      outside: 'outside'
+    },
+    filter: {
+      greyscale: 'greyscale',
+      sepia: 'sepia',
+      negative: 'negate',
+      duotone: 'duotone'
+    },
+    mask: {
+      circle: 'circle',
+      ellipse: 'ellipse',
+      triangle: 'triangle',
+      'triangle-180': 'triangle-180',
+      pentagon: 'pentagon',
+      'pentagon-180': 'pentagon-180',
+      hexagon: 'hexagon',
+      square: 'square',
+      star: 'star',
+      heart: 'heart'
+    }
+  },
+  joinWith: '&',
+  formatter: (key, value) => `${key}=${value}`
+} as OperationGeneratorConfig)
+
+export interface WeservOptions extends ImageOptions {
+    /**
+     * The url of your site that is exposed to the internet.
+     */
+    baseURL: string;
+
+    /**
+     * The url of the weserv service.
+     *
+     * @default https://wsrv.nl
+     */
+    weservURL?: string;
+}
+
+export const getImage: ProviderGetImage = (src, options: Partial<WeservOptions>) => {
+  const filename = src.substring(src.lastIndexOf('/') + 1)
+
+  if (typeof options.baseURL !== 'string' || options.baseURL.length === 0) {
+    if (import.meta.dev) {
+      throw createError({
+        statusCode: 500,
+        statusMessage: 'Internal Server Error',
+        message: 'The weserv provider requires the baseURL of your website.',
+        data: {
+          provider: 'weserv',
+          src,
+          modifiers: options.modifiers
+        },
+        fatal: true,
+        name: 'NuxtImageError'
+      })
+    } else {
+      // fall back to the original image in production
+      return { url: src }
+    }
+  }
+
+  // https://images.weserv.nl/docs/quick-reference.html
+  const operations = operationsGenerator({
+    filename,
+    we: 'true',
+    ...options.modifiers as Record<string, string>,
+    url: withBase(src, options.baseURL)
+  })
+    .replace('=true', '')
+
+  return {
+    url: withBase(
+      operations.length ? '?' + operations : '',
+      options.weservURL ?? 'https://wsrv.nl'
+    )
+  }
+}


Test patch:
diff --git a/test/e2e/__snapshots__/no-ssr.test.ts.snap b/test/e2e/__snapshots__/no-ssr.test.ts.snap
index bd3edae..e0d7529 100644
--- a/test/e2e/__snapshots__/no-ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/no-ssr.test.ts.snap
@@ -578,3 +578,20 @@ exports[`browser (ssr: false) > wagtail should render images 2`] = `
   "https://cms.demo.nypr.digital/images/329944/fill-200x200-c100%7Cformat-webp%7Cwebpquality-70",
 ]
 `;
+
+exports[`browser (ssr: false) > weserv should render images 1`] = `
+[
+  "https://wsrv.nl/?filename=200&we&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+]
+`;
+
+exports[`browser (ssr: false) > weserv should render images 2`] = `
+[
+  "https://wsrv.nl/?filename=200&we&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+]
+`;
diff --git a/test/e2e/__snapshots__/ssr.test.ts.snap b/test/e2e/__snapshots__/ssr.test.ts.snap
index 9bfd3da..b14e0d0 100644
--- a/test/e2e/__snapshots__/ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/ssr.test.ts.snap
@@ -578,3 +578,20 @@ exports[`browser (ssr: true) > wagtail should render images 2`] = `
   "https://cms.demo.nypr.digital/images/329944/fill-200x200-c100%7Cformat-webp%7Cwebpquality-70",
 ]
 `;
+
+exports[`browser (ssr: true) > weserv should render images 1`] = `
+[
+  "https://wsrv.nl/?filename=200&we&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+]
+`;
+
+exports[`browser (ssr: true) > weserv should render images 2`] = `
+[
+  "https://wsrv.nl/?filename=200&we&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&output=webp&url=https://picsum.photos/200",
+  "https://wsrv.nl/?filename=200&we&mask=circle&filt=sepia&output=webp&url=https://picsum.photos/200",
+]
+`;
diff --git a/test/providers.ts b/test/providers.ts
index 65eeea7..4a0e1c6 100644
--- a/test/providers.ts
+++ b/test/providers.ts
@@ -27,6 +27,7 @@ export const images = [
     wagtail: { url: '329944/original|format-webp|webpquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png' },
     hygraph: { url: 'https://media.graphassets.com/JL6e2yJERUyQtTiZIzPb' }
   },
@@ -58,6 +59,7 @@ export const images = [
     wagtail: { url: '329944/width-200|format-webp|webpquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200/JL6e2yJERUyQtTiZIzPb' }
   },
@@ -89,6 +91,7 @@ export const images = [
     wagtail: { url: '329944/height-200|format-webp|webpquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?height=200' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/x200/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&h=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?h=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=height:200/JL6e2yJERUyQtTiZIzPb' }
   },
@@ -120,6 +123,7 @@ export const images = [
     wagtail: { url: '329944/fill-200x200-c0|format-webp|webpquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x200/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200,height:200/JL6e2yJERUyQtTiZIzPb' }
   },
@@ -151,6 +155,7 @@ export const images = [
     wagtail: { url: '329944/fill-200x200-c0|format-webp|webpquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/resize/200x200/-/stretch/off/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit' },
     hygraph: { url: 'https://media.graphassets.com/auto_image/resize=width:200,height:200,fit:max/JL6e2yJERUyQtTiZIzPb' }
   },
@@ -182,6 +187,7 @@ export const images = [
     wagtail: { url: '329944/fill-200x200-c0|format-jpeg|jpegquality-70' },
     directus: { url: '/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?width=200&height=200&fit=contain&format=jpg' },
     uploadcare: { url: 'https://ucarecdn.com/c160afba-8b42-45a9-a46a-d393248b0072/-/format/jpeg/-/resize/200x200/-/stretch/off/' },
+    weserv: { url: 'https://wsrv.nl/?filename=test.png&we&w=200&h=200&fit=contain&output=jpg&url=https://my-website.com/test.png' },
     sirv: { url: 'https://demo.sirv.com/test.png?w=200&h=200&scale.option=fit&format=jpg' },
     hygraph: { url: 'https://media.graphassets.com/output=format:jpg/resize=width:200,height:200,fit:max/JL6e2yJERUyQtTiZIzPb' }
   }
diff --git a/test/unit/providers.test.ts b/test/unit/providers.test.ts
index acf4785..c924dac 100644
--- a/test/unit/providers.test.ts
+++ b/test/unit/providers.test.ts
@@ -7,6 +7,7 @@ import { images } from '../providers'
 import { cleanDoubleSlashes } from '#image/utils'
 import * as ipx from '#image/providers/ipx'
 import * as none from '~/src/runtime/providers/none'
+import * as weserv from '~/src/runtime/providers/weserv'
 import * as aliyun from '#image/providers/aliyun'
 import * as awsAmplify from '#image/providers/awsAmplify'
 import * as cloudflare from '#image/providers/cloudflare'
@@ -429,17 +430,33 @@ describe('Providers', () => {
       expect(generated).toMatchObject(image.uploadcare)
     }
   })
+
   it('sirv', () => {
     const providerOptions = {
       baseURL: 'https://demo.sirv.com'
     }
-
     for (const image of images) {
       const [src, modifiers] = image.args
       const generated = sirv.getImage(src, { modifiers, ...providerOptions }, emptyContext)
       expect(generated).toMatchObject(image.sirv)
     }
   })
+
+  it('weserv', () => {
+    const providerOptions = {
+      baseURL: 'https://my-website.com/'
+    }
+
+    for (const image of images) {
+      const [src, modifiers] = image.args
+      const generated = weserv.getImage(src, { modifiers, ...providerOptions }, emptyContext)
+      expect(generated).toMatchObject(image.weserv)
+    }
+
+    const generated = weserv.getImage('test.png', {}, emptyContext)
+    expect(generated).toMatchObject({ url: 'test.png' })
+  })
+
   it('none', () => {
     const providerOptions = {
     }