npm package: @20dimensions/vite-plugin-liquid-image
Description: A framework-agnostic LiquidJS {% optimized_image %} tag + Vite pipeline for responsive WebP/AVIF images using Sharp
Latest version: 0.1.7

# @20dimensions/vite-plugin-liquid-image

[![npm](https://img.shields.io/npm/v/@20dimensions/vite-plugin-liquid-image?color=blue&logo=npm)](https://www.npmjs.com/package/@20dimensions/vite-plugin-liquid-image)
[![license](https://img.shields.io/npm/l/@20dimensions/vite-plugin-liquid-image)](./LICENSE)

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`, injecting `width` and `height` inline to prevent CLS.
- **Modern Formats:** Automatically generates `WebP` and `AVIF` alongside 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

```bash
npm install @20dimensions/vite-plugin-liquid-image
```

## Tag Usage

The Liquid tag is designed to feel like a standard HTML `<img>`, but vastly more powerful.

```liquid
{% 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"` or `class='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`**:
```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

```liquid
{% 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:

```liquid
{% 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

```js
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`**:
```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
1. When Liquid parses `{% optimized_image %}`, it constructs a `<picture>` element with `srcset` URLs pointing to `avif` and `webp` variants.
2. Each request is cached in a module-level store.
3. During local Vite `dev`, a middleware intercepts these asset URLs and processes images on-the-fly with `sharp`.
4. During Vite `build`, the `writeBundle` hook 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.