# Optimized images

> Your results' photos resized to the size each page shows them at and sent as WebP, with a soft preview of each photo until it loads. Included in every paid plan.

A search answers in milliseconds, then shoppers wait for photos far larger than the page shows them. With optimized images on, Findlane resizes the images in your records to the size each page shows them at and sends them as WebP from `images.findlane.dev`. On the first stores to use it, the resized photos were 88% smaller than the originals.

Every result also gets a **placeholder**: a soft, blurred preview of its photo, about 25 bytes, shown in the photo's place until it loads. The page keeps its shape while photos arrive, and results never show as empty boxes.

Your records keep their own image addresses. The [React components](/docs/react.md), the [search box script](/docs/search-box.md) and the [Shopify app](/docs/shopify.md) load optimized images by themselves; on your own pages it's [a few lines](#on-your-pages).

Every paid plan includes optimized images: [see what counts](#what-counts).

## Turn them on

In the dashboard, open an index, choose **Access**, and go to **Optimized images**:

1. **Image hosts**: the hosts your records' image addresses are on, such as `cdn.shop.example.com`. The page lists the hosts it finds in your records; choose one, or add it.
2. **Image field**: the field placeholders are made from. It's `image` unless you choose another; a list's items are written as `images.src`.
3. **Save.**

Placeholders are made within a few minutes, and for new records as they're written. The group shows how many records have one, and how many of the month's copies the workspace has made. To turn optimized images off, remove every host.

Agents use the `set_image_hosts` [tool](/docs/agents.md). Servers use the [API](/docs/api.md#browser-search-access) with an API key that has the `write` scope:

```sh
curl -X PUT "$API/api/teams/$FINDLANE_WORKSPACE_ID/indexes/products/public-access/images" \
  -H "Authorization: Bearer $FINDLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hosts": ["cdn.shop.example.com"], "field": "image" }'
```

### Which images

Only images that are in the index's records, on its image hosts, are resized: the image field, and also variant photos, galleries and any other field. Anything else loads as it is, so nobody can use your copies for other images from your hosts. A subindex's images follow its index's setting.

Findlane fetches the originals as `FindlaneImages/1.0`, once for each width it makes. If your image host turns away unknown clients, let that user agent through.

### Content Security Policy

If your site sends a `Content-Security-Policy`, allow Findlane's addresses in it:

```text
connect-src https://api.findlane.dev;
img-src https://images.findlane.dev;
```

`images.findlane.dev` also serves the [banner images](/docs/settings.md#merchandising) you upload. Pages using the browser SDK before 0.28.0, or the search box script before 1.4.0, load optimized images from `https://api.findlane.dev` instead, so allow it in `img-src` too until they're updated.

## On your pages

Search answers name the hosts whose images can be resized, in their `x-findlane-images` header, and each hit carries its `placeholder`.

### React

`Image` from `@findlane/browser/react` (0.27.1) shows a record's photo resized to the width it's shown at, and twice that for sharp screens. With the hit's placeholder, the preview shows until the photo loads, and the photo sharpens in:

```tsx
import { Image } from "@findlane/browser/react"

<Image src={hit.record.image} width={240} placeholder={hit.placeholder} alt={hit.record.title} />
```

It loads lazily, and if the resized photo fails to load, it loads the original instead. Inside `SearchProvider`, `SearchBox` or `Recommendations` it uses their client; elsewhere, pass `client`. The components' own results and recommendations already use it.

### Your own markup

`client.imageUrl(src, width)` with the [browser SDK](/docs/browser-sdk.md#imageurlsrc-width) (0.28.0) gives an image's resized address, or `src` as it is when its host isn't allowed. It learns the hosts from the first search answer, so build image addresses from results. Load them with `crossorigin="anonymous"`, which uses the connection the client opens for them, and fall back to the original if one fails:

```ts
const { hits } = await client.search({ q: "rain jacket" })
for (const hit of hits) {
  const src = String(hit.record.image)
  const img = document.createElement("img")
  img.src = client.imageUrl(src, 240)
  img.srcset = `${client.imageUrl(src, 240)} 1x, ${client.imageUrl(src, 480)} 2x`
  img.crossOrigin = "anonymous"
  img.onerror = () => { img.onerror = null; img.removeAttribute("srcset"); img.src = src }
  list.append(img)
}
```

Before a page's first search answers, the client resizes with what an earlier answer said in the same browser (0.28.1), so results a page shows again after a reload or Back are resized too. On a first visit, a page that shows records' photos before it searches (a home page's products, say) can name the hosts it knows, and they're resized from the first render (0.29.0):

```ts
const client = new SearchBrowserClient({ publicId: "pub_your_public_id", imageHosts: ["cdn.shop.example"] })
```

From the first answer on, the index's own settings decide, so turning optimized images off still works without a code change.

For the preview, `placeholderUrl(hit.placeholder)` draws it as a small PNG data address, and `placeholderCss(url)` is the style to put on the image while it loads. When it loads, set `revealCss` for 350 ms, then remove the style:

```ts
import { placeholderCss, placeholderUrl, revealCss } from "@findlane/browser"

const preview = placeholderUrl(hit.placeholder)
if (preview) {
  img.style.cssText = placeholderCss(preview)
  img.addEventListener("load", () => {
    img.style.cssText = revealCss
    setTimeout(() => img.removeAttribute("style"), 350)
  }, { once: true })
}
```

### Sizes and formats

- **Widths** round up to one of 15: 48, 64, 96, 128, 160, 192, 256, 320, 384, 480, 640, 768, 960, 1,280 and 1,600 pixels. Pages share copies, and odd widths don't make new ones.
- **Never larger than the original:** an image isn't enlarged past its own width, and a copy that would be bigger than the original is sent as the original. For sharp large photos, give records the full-size photo's address.
- **Formats:** WebP to browsers that read it, which is every current one; otherwise the original's kind. JPEG, PNG and WebP are resized. GIF, AVIF, SVG and very large images load as they are.
- **Changes:** a copy is checked against its original after a day, so a photo replaced at the same address shows within about two days. A new address is a new image.

### Server rendering

The [server SDK](/docs/server-sdk.md) doesn't read the header, so a page rendered on the server shows original images until its first search in the browser. Placeholders do come with server-rendered hits.

### Shopify

Shopify's CDN already sizes Shopify's images, and the app's search box asks it for each size. Turning optimized images on, with `cdn.shopify.com` as the host, adds the placeholders.

## What counts

Each paid plan includes 5 new copies a month for each record it includes. A copy is one image at one width, made the first time a page shows it at that width.

| Plan    | New copies a month |
| ------- | ------------------ |
| Free    | Not included       |
| Starter | 250,000            |
| Team    | 750,000            |
| Growth  | 2,500,000          |
| Scale   | 5,000,000          |

- **Free to load:** copies already made, from any page or data center, don't count. Neither do placeholders, or images sent as they are.
- **Typical use:** a store shows each photo at two or three widths, such as a result at 1× and 2× and a suggestion's thumbnail. Its first month makes most of its copies; after that, mostly new products and changed photos do. A copy no page has used for 30 days is removed, and made again when one does.
- **At 80%**, owners get an email. **At the month's copies**, photos without a copy yet load as their originals until the 1st of the next month, while copies already made keep loading. Settings shows the month's copies under Usage.
