Skip to content
Start free

Guides

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, the search box script and the Shopify app load optimized images by themselves; on your own pages it's a few lines.

Every paid plan includes optimized images: see 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. Servers use the API with an API key that has the write scope:

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:

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

images.findlane.dev also serves the banner images 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:

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 (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:

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):

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:

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 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.

PlanNew copies a month
FreeNot included
Starter250,000
Team750,000
Growth2,500,000
Scale5,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.