@zatiq/image
v1.0.0
Published
Responsive image toolkit for Zatiq's Cloudflare Image Resizer CDN
Readme
@zatiq/image
Responsive image toolkit for Zatiq internal projects.
Generates optimized URLs for the Cloudflare Image Resizer CDN at img.zatiqeasy.com.
Install
# from the monorepo root (or link locally)
bun add @zatiq/imagePackage Exports
| Import path | Contents | React required? |
| --------------------- | ----------------------------------------------- | --------------- |
| @zatiq/image | URL builders, types, presets, constants | No |
| @zatiq/image/react | <ZatiqImage>, provider, hook + all core utils | Yes |
| @zatiq/image/presets | Preset definitions only | No |
The core (@zatiq/image) is framework-agnostic — use it in server components, API routes, scripts, or any non-React context.
Quick Start
1. Add the provider (once, at the root)
// app/providers.tsx
import { ZatiqImageProvider } from "@zatiq/image/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ZatiqImageProvider config={{ defaultQuality: 85 }}>
{children}
</ZatiqImageProvider>
);
}The provider is optional. Components fall back to defaults if omitted:
CDN origin =https://img.zatiqeasy.com, quality = 85, format =auto.
2. Use the component
import { ZatiqImage } from "@zatiq/image/react";
// Preset mode — one prop, zero config
<ZatiqImage imageKey="products/shoe-001.jpg" alt="Running shoe" preset="productCard" />
// Manual mode — full control
<ZatiqImage
imageKey="products/shoe-001.jpg"
alt="Running shoe"
widths={[400, 800, 1200]}
sizes="(max-width: 768px) 100vw, 50vw"
formats={["avif", "webp"]}
options={{ fit: "cover", q: 90 }}
/>API Reference
<ZatiqImage> Component
Common Props
| Prop | Type | Default | Description |
| -------------- | -------- | -------- | ----------------------------------------------- |
| imageKey | string | — | Required. R2 object key |
| alt | string | — | Required. Alt text |
| aspectRatio | string | — | CSS aspect-ratio for CLS prevention ("16/9") |
| width | number | — | HTML width attribute for layout |
| height | number | — | HTML height attribute for layout |
| loading | string | "lazy" | Native lazy loading |
| className | string | — | CSS class |
Preset Mode
| Prop | Type | Description |
| ----------- | -------- | ----------------------------------------- |
| preset | string | Preset name (see Built-in Presets below) |
| overrides | object | Override preset defaults per-instance |
Manual Mode
| Prop | Type | Description |
| --------- | --------------- | -------------------------------------------------------- |
| widths | number[] | Required. Width breakpoints |
| sizes | string | Required. HTML sizes attribute |
| formats | ImageFormat[] | Renders <picture> with <source> per format |
| options | object | Transform options (fit, q, f, g, etc.) |
When formats is provided, the component renders:
<picture>
<source type="image/avif" srcset="..." sizes="..." />
<source type="image/webp" srcset="..." sizes="..." />
<img src="..." srcset="..." sizes="..." />
</picture>Without formats, a plain <img> with f=auto is rendered — the Worker negotiates the best format via the Accept header. This is the recommended approach for most cases.
Built-in Presets
| Name | Widths (px) | Sizes | Defaults |
| --------------- | ------------------------- | ----------------------------------------------------- | ------------------------ |
| hero | 480, 768, 1024, 1440, 1920 | 100vw | cover, auto |
| productCard | 300, 450, 600 | (max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw | cover, q80 |
| productDetail | 400, 600, 800, 1200 | (max-width: 768px) 100vw, 50vw | contain, auto |
| thumbnail | 80, 120, 160 | 80px | cover, q75 |
| avatar | 48, 96, 144 | 48px | cover, auto |
| banner | 480, 768, 1200 | (max-width: 768px) 100vw, 80vw | cover, auto |
Adding Custom Presets
<ZatiqImageProvider
config={{
presets: {
// Adds to (or overrides) built-in presets
blogHero: {
widths: [640, 960, 1280, 1600],
sizes: "(max-width: 768px) 100vw, 960px",
defaults: { f: "auto", fit: "cover", q: 90 },
},
},
}}
>
{children}
</ZatiqImageProvider>Then use it like any built-in: <ZatiqImage preset="blogHero" ... />
useImageSrc Hook
For programmatic URL generation — backgrounds, OG images, CSS, non-JSX contexts.
const img = useImageSrc("products/shoe-001.jpg");
img.url({ w: 800 }) // → full CDN URL
img.srcSet([400, 800, 1200]) // → srcset string
img.dprSrcSet(48) // → "...48 1x, ...96 2x, ...144 3x"
img.lqip() // → tiny blurred placeholder URL
img.og() // → 1200×630 OG image URLCore URL Builders (framework-agnostic)
These work anywhere — server components, API routes, scripts.
import { buildUrl, buildSrcSet, buildLqipUrl, buildOgImageUrl } from "@zatiq/image";
const CDN = "https://img.zatiqeasy.com";
buildUrl(CDN, "products/shoe.jpg", { w: 800, f: "auto" });
// → "https://img.zatiqeasy.com/image/f=auto,w=800/products/shoe.jpg"
buildSrcSet(CDN, "products/shoe.jpg", [400, 800, 1200], { f: "auto", fit: "cover" });
// → "https://img.zatiqeasy.com/image/f=auto,fit=cover,w=400/... 400w, ..."
buildLqipUrl(CDN, "products/shoe.jpg");
// → "https://img.zatiqeasy.com/image/blur=20,f=auto,q=30,w=40/products/shoe.jpg"
buildOgImageUrl(CDN, "products/shoe.jpg");
// → "https://img.zatiqeasy.com/image/f=jpeg,fit=cover,h=630,q=85,w=1200/products/shoe.jpg"Recipes
Above-the-fold hero (no lazy load)
<ZatiqImage
imageKey="banners/summer-sale.jpg"
alt="Summer sale"
preset="hero"
loading="eager"
fetchPriority="high"
aspectRatio="16/9"
className="w-full object-cover"
/>Product grid with explicit AVIF → WebP fallback
<ZatiqImage
imageKey={product.imageKey}
alt={product.name}
widths={[300, 600, 900]}
sizes="(max-width: 640px) 50vw, 33vw"
formats={["avif", "webp"]}
options={{ fit: "cover" }}
aspectRatio="1/1"
className="rounded-lg"
/>CSS background with blur-up
function HeroSection({ imageKey }: { imageKey: string }) {
const img = useImageSrc(imageKey);
return (
<section
className="h-screen bg-cover bg-center"
style={{ backgroundImage: `url(${img.url({ w: 1920, fit: "cover" })})` }}
/>
);
}Open Graph image in generateMetadata()
// app/products/[slug]/page.tsx
import { buildOgImageUrl } from "@zatiq/image";
export async function generateMetadata({ params }) {
const product = await getProduct(params.slug);
return {
openGraph: {
images: [buildOgImageUrl("https://img.zatiqeasy.com", product.imageKey)],
},
};
}Preset override per-instance
<ZatiqImage
imageKey="products/premium-leather.jpg"
alt="Premium leather bag"
preset="productDetail"
overrides={{ q: 95, g: "auto" }}
/>Generated HTML Examples
Preset mode (preset="productCard")
<img
src="https://img.zatiqeasy.com/image/f=auto,fit=cover,q=80,w=300/products/shoe.jpg"
srcset="
https://img.zatiqeasy.com/image/f=auto,fit=cover,q=80,w=300/products/shoe.jpg 300w,
https://img.zatiqeasy.com/image/f=auto,fit=cover,q=80,w=450/products/shoe.jpg 450w,
https://img.zatiqeasy.com/image/f=auto,fit=cover,q=80,w=600/products/shoe.jpg 600w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="Running shoe"
loading="lazy"
decoding="async"
/>Manual mode with formats
<picture>
<source
type="image/avif"
srcset="
https://img.zatiqeasy.com/image/f=avif,fit=cover,w=400/products/shoe.jpg 400w,
https://img.zatiqeasy.com/image/f=avif,fit=cover,w=800/products/shoe.jpg 800w"
sizes="(max-width: 768px) 100vw, 50vw" />
<source
type="image/webp"
srcset="
https://img.zatiqeasy.com/image/f=webp,fit=cover,w=400/products/shoe.jpg 400w,
https://img.zatiqeasy.com/image/f=webp,fit=cover,w=800/products/shoe.jpg 800w"
sizes="(max-width: 768px) 100vw, 50vw" />
<img
src="https://img.zatiqeasy.com/image/f=auto,fit=cover,w=400/products/shoe.jpg"
srcset="..."
sizes="(max-width: 768px) 100vw, 50vw"
alt="Running shoe"
loading="lazy"
decoding="async" />
</picture>Architecture
@zatiq/image
├── Core (framework-agnostic)
│ ├── url-builder ← pure functions, zero deps
│ ├── presets ← preset definitions
│ ├── types ← all TypeScript types
│ └── constants ← defaults, MIME map
│
└── React layer (@zatiq/image/react)
├── ZatiqImage ← <img>/<picture> component (memo'd)
├── Provider ← project-level config context
└── useImageSrc ← hook for programmatic URLsThe core has zero dependencies and works in any JS runtime.
The React layer is a thin wrapper (~3KB) that reads config from context.
