@teamnovu/nuxt-image-module
v1.0.0-beta.3
Published
Responsive image module for Nuxt 4 with provider-agnostic focal-point handling and a Statamic adapter built on @nuxt/image.
Readme
@teamnovu/nuxt-image-module
Responsive images for Nuxt 4. Builds on @nuxt/image for provider-agnostic URL generation, ships a JS-driven sizes strategy for accurate per-element widths, and includes a Statamic adapter so <NovuStatamicImage :src="asset" /> handles the common case end-to-end.
Highlights
- Single-image element with provider-agnostic responsive
srcset. sizesstarts at1px(so SSR ships a tiny blurred placeholder candidate) and is upgraded client-side to the actual rendered element width inpx, recomputed viaResizeObserver. Forobject-fit: coverimages that are wider than their container, the value reflects the cropped image's true width (containerHeight * imageAspect) so the browser picks a candidate big enough to fill the visible area without upscaling.- Five components, layered:
<NovuImage>- provider-agnostic base<NovuCloudinaryImage>- Cloudinary-flavoured (focal: 'face',zoom,crop, rawtransforms)<NovuBunnyImage>- Bunny-flavoured (faceCrop,cropGravity,sharpen,blur)<NovuImgproxyImage>- imgproxy-flavoured (preset,gravity,resizingType,extend)<NovuStatamicImage>- Statamic adapter that auto-resolves to the matching wrapper (or<NovuImage>when no wrapper matches)
- Built-in imgproxy provider (vendored from nuxt/image#2117 until it merges) with HMAC URL signing.
- Performance defaults:
loading="lazy",decoding="async", inlineaspect-ratioCSS for CLS prevention. Opt-inpriorityflips toloading="eager"+fetchpriority="high".
Install
pnpm add @teamnovu/nuxt-image-moduleexport default defineNuxtConfig({
modules: ['@teamnovu/nuxt-image-module'],
novuImage: {
provider: 'cloudinary',
},
image: {
cloudinary: {
baseURL: 'https://res.cloudinary.com/your-cloud/image/upload/',
},
// bunny: { baseURL: 'https://your-zone.b-cdn.net' },
// imgproxy: { baseURL: 'https://img.novu.run' },
},
})The module installs @nuxt/image for you and auto-registers the imgproxy provider. Configure image.<provider> exactly as documented in the @nuxt/image docs.
Module options (novuImage)
| Option | Type | Default | Notes |
| -------------------- | ------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| screens | Record<string, number\|string> | { xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1600, '3xl': 2000 } | Each entry generates one srcset candidate at that pixel width. |
| fallbackWidth | number | 2000 | Width used for the legacy src URL. |
| usePlaceholder | boolean | true | Inlines a low-quality candidate (descriptor 32w) into srcset. |
| placeholderQuality | number | 30 | Quality of the inlined placeholder. |
| placeholderWidth | number | 300 | Width of the inlined placeholder. |
| defaultQuality | number\|string? | - | Optional global quality modifier. Leave unset to defer to the provider's own default (Cloudinary auto-applies q_auto, etc.). |
| defaultFormat | string? | - | Optional global format modifier. Leave unset to defer to the provider's own default (Cloudinary auto-applies f_auto, imgproxy uses webp, etc.). |
| provider | string? | - | Default @nuxt/image provider; also drives <NovuStatamicImage> wrapper resolution. |
| statamicPlaceholderFields | string[] | ['placeholder', 'lqip', 'thumbhash'] | Statamic asset field handles checked for daun/statamic-placeholders data. |
Components
<NovuImage> - provider-agnostic base
<NovuImage
src="path/to/image.jpg"
alt="A cat"
:width="1600"
:height="900"
:focal="[1200, 660]"
provider="cloudinary"
/>Common props (all optional unless noted):
| Prop | Type | Notes |
| ------------- | ----------------------------- | -------------------------------------------------------------------- |
| src | string (required) | Asset key/path passed to the configured provider. |
| alt | string | Alt text. |
| width/height| number\|string | Rendered image dimensions. Used for the aspect-ratio CSS hint and candidate height derivation. |
| sourceWidth/sourceHeight | number\|string | Original source dimensions. Used to map source-pixel focal points for providers such as imgproxy when rendered dimensions differ from the source. |
| aspectRatio | number | Explicit aspect ratio (overrides width/height). |
| focal | 'auto' \| [number, number] | Source-pixel focal point, translated per provider. Providers with native auto focal support default to 'auto'. |
| provider | string | Override the default provider for this element. |
| quality | number\|string | Forwarded to useImage. |
| format | string | Forwarded to useImage. |
| fit | string | Forwarded to useImage. |
| modifiers | Record<string, unknown> | Raw modifiers escape hatch. |
| sizes | string | Hard override for the dynamic sizes calculation. |
| priority | boolean | Sets loading="eager" + fetchpriority="high". |
| loading | 'lazy' \| 'eager' | Defaults to 'lazy' (or 'eager' when priority). |
| decoding | 'async' \| 'sync' \| 'auto' | Defaults to 'async'. |
focal tuples are source image pixel coordinates, for example [1200, 660] on a 4000x2667 source. When the rendered width/height differ from the source dimensions, pass sourceWidth/sourceHeight as well so providers that need normalized coordinates, such as imgproxy, can map the focal point correctly. <NovuStatamicImage> fills these source dimensions from the asset automatically. Cloudinary and imgproxy default to their native auto focal modes; Bunny does not, because its face_crop API is a separate explicit behavior.
<NovuCloudinaryImage>
Pins provider="cloudinary" and adds Cloudinary-only conveniences. Most importantly, focal="face" triggers Cloudinary's face-detection gravity.
| Prop | Type | Notes |
| ---------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| crop | string | Cloudinary crop mode (URL parameter c_). Accepts both Cloudinary-native names ('lfill', 'thumb') and @nuxt/image's aliases ('cover', 'thumbnail'). |
| focal | 'auto' \| 'face' \| [x, y] | Adds 'face' on top of the agnostic focal values. |
| zoom | number \| string | Cloudinary zoom factor (z_<value>). |
| transformation | string \| Record<string, any> | Named Cloudinary transformation preset (t_<name>), or a raw modifiers object spread into useImage's modifiers. |
<NovuCloudinaryImage
src="profile.jpg"
alt="Profile photo"
:width="800"
:height="800"
focal="face"
crop="thumb"
zoom="0.7"
/><NovuBunnyImage>
Pins provider="bunny". faceCrop maps to Bunny's face-detection face_crop parameter, while cropGravity maps directly to Bunny's documented crop_gravity anchors (center, north, south, east, west, northeast, northwest, southeast, southwest). Pixel-coordinate focal points are ignored because Bunny's manual focal crop is not wired yet.
<NovuBunnyImage
src="hero.jpg"
:width="1600"
:height="900"
face-crop
:sharpen="true"
/><NovuImgproxyImage>
Pins provider="imgproxy". Use gravity to pass an imgproxy-native gravity string (e.g. 'fp:0.3:0.25'); when gravity is set the agnostic focal mapping is bypassed for that element.
<NovuImgproxyImage
src="https://origin.example.com/photo.jpg"
:width="1600"
:height="900"
preset="hero"
resizing-type="fill"
/><NovuStatamicImage> - the 90% case
Renders any Statamic image asset. Auto-resolves to the matching provider wrapper (provider="cloudinary" -> <NovuCloudinaryImage>, etc.) and falls back to <NovuImage> when no wrapper matches.
<NovuStatamicImage :src="entry.hero_image" />
<NovuStatamicImage :src="entry.hero_image" provider="bunny" />
<NovuStatamicImage :src="user.avatar" :fallback-alt="user.name" />
<NovuStatamicImage :src="entry.hero_image" placeholder-field="hero_lqip" />Alt-text resolution order
props.alt(hard override).asset.alt_${locale}if a locale is detected fromuseNuxtApp().$i18n.localeor set via thelocaleprop.asset.alt.props.fallbackAlt(use this for things likeuser.name).undefined.
Focal-point resolution order
props.focal(hard override).asset.focus(Statamic's"x-y"percent format).asset.focus_css(Statamic's"x% y%"format - some sites add this via augmentation).undefined(provider falls back to its own default - usually centre).
The dead-centre default (focus: '50-50') is treated as "no focal point" so providers can pick their own smarter centre/strategy.
Statamic placeholders
<NovuStatamicImage> can consume placeholder data generated by daun/statamic-placeholders. By default it checks the placeholder, lqip, and thumbhash asset fields; override the checked handles globally with novuImage.statamicPlaceholderFields or per component with placeholderField.
Query the ready-to-use data URI when payload size is not a concern:
... on Asset_Assets {
alt
lqip {
uri
}
}Or query only the ThumbHash hash and let the Nuxt component decode it:
... on Asset_Assets {
alt
lqip {
type
hash
}
}When both uri and hash are present, uri wins. Hash decoding is supported for ThumbHash placeholders only; BlurHash or average-color placeholders need to provide uri. If a Statamic site does not have the addon or the queried asset object has no placeholder field, rendering falls back to the existing generated low-quality srcset placeholder. Only include placeholder subfields in GraphQL queries for Statamic schemas where the addon field exists, because GraphQL itself rejects unknown fields before this component can handle the missing data.
Statamic blueprint requirements
The <NovuStatamicImage> component reads the following fields from a Statamic asset object:
permalink(required) - absolute URL; the host is stripped so the configured provider'sbaseURLapplies.width/height- required to convertfocuspercentages to pixel coordinates (and to computeaspect-ratio).alt- default alt text.alt_<locale>(e.g.alt_de,alt_fr) - localised alt text. Required only if you want per-locale alts.focus- Statamic's native"x-y"percent string. Optional.placeholder/lqip/thumbhash- optional placeholder fields fromdaun/statamic-placeholders, either as{ uri }or{ type, hash }.
Composable
resolveStatamicAsset(src, props?, options?) is a pure helper exposed for cases where you want to compute the resolved { src, placeholderSrc, alt, focal, width, height, aspectRatio } outside of the component (for SEO heads, OG images, etc.).
import { resolveStatamicAsset } from '#imports'
const resolved = resolveStatamicAsset(asset, { fallbackAlt: 'Article hero' }, { locale: 'de' })A note on the imgproxy provider
The imgproxy provider used here is vendored from nuxt/image#2117. When that PR merges into @nuxt/image, the vendored version (and the @noble/hashes runtime dependency) will be removed.
Development
pnpm dev:prepare # build types + prepare playground
pnpm dev # run the playground
pnpm test # run vitest
pnpm lint # run eslint
pnpm test:types # vue-tsc on src/ and playground/License
MIT
