@20dimensions/vite-plugin-liquid-image
v0.1.7
Published
A framework-agnostic LiquidJS {% optimized_image %} tag + Vite pipeline for responsive WebP/AVIF images using Sharp
Downloads
486
Readme
@20dimensions/vite-plugin-liquid-image
A high-performance image optimization plugin for any Vite + LiquidJS project.
It seamlessly connects LiquidJS templating to Vite's build pipeline, using sharp to automatically generate responsive HTML <picture> elements with WebP and AVIF variants, while offering on-the-fly image processing during local development!
Features
- Art Direction & Presets: Define named presets for different UI components (e.g., "card", "hero") with specific breakpoints, aspect ratios, and cropping focus.
- Responsive Generation: Emits optimized
<picture>sets based on configurable width multipliers (e.g., 1x, 2x). - Default "Original Size" Logic: If no widths are configured, the plugin automatically detects the image's native width and applies multipliers from there.
- Aspect Ratio & Cropping: Automatically crop images to specific aspect ratios (e.g.,
16:9,1:1) with controllable focus points. - Layout Shift Prevention: Automatically reads native dimensions with
sharp, injectingwidthandheightinline to prevent CLS. - Modern Formats: Automatically generates
WebPandAVIFalongside your original JPEG/PNG files. - Vite Integration: Captures image requests from Liquid tags and intercepts them in Vite's Dev Server for instant on-the-fly previews.
- Static Asset Compilation: Crunches and outputs optimized binaries into
dist/assets/images/during production builds. - Native HTML Feel: Drop it in your template with standard attributes (
sizes,loading,class,alt). - Framework Agnostic: Designed to work with any Vite setup utilizing LiquidJS.
Installation
npm install @20dimensions/vite-plugin-liquid-imageTag Usage
The Liquid tag is designed to feel like a standard HTML <img>, but vastly more powerful.
{% optimized_image "src/images/hero.jpg" alt="A beautiful hero" class="rounded-xl" sizes="(max-width: 600px) 100vw, 1200px" loading="eager" %}Source Argument
The first argument is the image source. It can be:
- A quoted string:
"src/images/hero.jpg"or'src/images/hero.jpg' - A Liquid variable:
featured_image(resolved at render time) - A Liquid expression with filters:
product.image | append: ".jpg" - An explicit keyword:
src=my_variable
HTML Attributes
You can pass any valid HTML attribute after the source:
- Quoted:
alt="Hero Image"orclass='rounded shadow' - Unquoted variable:
alt=i18n.hero.imageAlt(Liquid dotted path, resolved at render time)
Commonly used:
| Attribute | Description |
|---|---|
| sizes="..." | Highly recommended. Tells the browser which variant to load. Defaults to 100vw. |
| alt="..." alt=i18n.path | Accessibility text. |
| class="..." | CSS styling hooks. |
| loading="lazy" | Native lazy loading. |
| width=N / height=N | Override injected dimensions. If only one is given, the other is auto-calculated from the original ratio. |
| preset="name" | Use an art direction preset. |
Configuration
vite.config.js:
import { defineConfig } from 'vite';
import { staticImagePlugin } from '@20dimensions/vite-plugin-liquid-image';
export default defineConfig({
plugins: [
staticImagePlugin({
formats: ['avif', 'webp', 'original'],
widths: [], // Default: [] = uses original image width as base
multipliers: [1, 2], // Generates 1x and 2x (retina) variants for every width
quality: 80,
injectDimensions: true,
baseDir: process.cwd(), // Base dir for resolving source image paths
tagName: 'optimized_image', // Optionally rename the Liquid tag
// Art Direction Presets
presets: {
card: [
{
media: '(max-width: 480px)',
width: 400,
sizes: '100vw',
aspectRatio: '2:3',
position: 'top'
},
{
media: 'default',
width: 376,
sizes: '(max-width: 1024px) 50vw, 376px'
}
]
}
})
],
});How widths are determined
| Scenario | Base width |
|---|---|
| Preset | width defined per breakpoint |
| widths: [400, 800] | Those exact widths |
| widths: [] (default) | Original image's native width (auto-detected via sharp) |
In all cases, multipliers (default [1, 2]) are applied. A 1000px image with defaults generates 1000w and 2000w variants. Upscaling is supported.
Art Direction & Presets
{% optimized_image "src/images/product.png" preset="card" alt="A cool product" %}When a preset is used, the tag loops through each breakpoint and multiplier and generates <source> elements with appropriate media and srcset attributes.
Liquid Variable Expressions
The source argument can be any Liquid expression:
{% comment %} Simple variable {% endcomment %}
{% optimized_image featured_image alt="Hero" %}
{% comment %} With filters {% endcomment %}
{% optimized_image image_name | append: "-hero" sizes="100vw" %}
{% comment %} Passed from parent template {% endcomment %}
{% render 'card', image: product.cover_image %}
{# in card.liquid: #}
{% optimized_image image preset="card" %}Standalone LiquidJS Integration
import { imageTag } from '@20dimensions/vite-plugin-liquid-image';
import { Liquid } from 'liquidjs';
const engine = new Liquid();
engine.plugin(imageTag({
baseDir: 'src',
tagName: 'optimized_image'
}));With @20dimensions/liquid-site-builder
Use the unified liquidImagePlugin wrapper to register both the Vite and LiquidJS pieces in one call:
vite.config.js:
import { defineConfig } from 'vite';
import { liquidSitePlugin } from '@20dimensions/liquid-site-builder/vite';
import { liquidImagePlugin } from '@20dimensions/vite-plugin-liquid-image';
export default defineConfig({
plugins: [
liquidSitePlugin({
domain: 'https://example.com',
defaultLanguage: 'en',
plugins: [
liquidImagePlugin({
formats: ['avif', 'webp', 'original'],
widths: [], // Uses original image width by default
multipliers: [1, 2],
quality: 80,
injectDimensions: true,
baseDir: process.cwd()
})
]
})
],
});How It Works
- When Liquid parses
{% optimized_image %}, it constructs a<picture>element withsrcsetURLs pointing toavifandwebpvariants. - Each request is cached in a module-level store.
- During local Vite
dev, a middleware intercepts these asset URLs and processes images on-the-fly withsharp. - During Vite
build, thewriteBundlehook processes the full queue and writes optimized binaries into Vite's output folder.
Contributing
Contributions are always welcome! Feel free to open an issue or submit a Pull Request.
