How to add Algolia search and Cloudinary media optimization to a Webflow Cloud app

Learn how to index Webflow CMS content in Algolia and serve optimized images from Cloudinary inside a Webflow Cloud Next.js app, step by step.

How to add Algolia search and Cloudinary media optimization to a Webflow Cloud app

Colin Lateano
Developer Evangelist
View author profile
Colin Lateano
Developer Evangelist
View author profile
Table of contents

A growing Webflow CMS collection eventually needs two things at once: fast search and right-sized images, and you can add both to a Webflow Cloud app with nothing but Route Handlers and fetch.

Once a Webflow CMS collection grows beyond a few hundred items, two needs emerge together. Visitors need fast search to find the right item, and images need to ship in the right format and size for each device. Algolia covers search, Cloudinary covers media, and both fit into a Webflow Cloud app running on the Workers runtime.

Both tools fit cleanly into Webflow Cloud's Workers runtime. Algolia's v5 JavaScript client ships a dedicated worker build that uses the Fetch API, so it works in a Route Handler without patching the HTTP layer. Cloudinary URL transformations require no SDK; they occur at Cloudinary's CDN edge when the URL is requested. The upload Route Handler uses plain fetch against Cloudinary's REST API.

The sections below set up both, starting with the Algolia index and ending with optimized image uploads.

What do you need to add Algolia search and Cloudinary media optimization to a Webflow Cloud app?

You need a Webflow Cloud Next.js project, an Algolia account with an index, and a Cloudinary account. Both tools have free tiers that work for development and low-traffic production use.

Here’s the full list:

  • A Webflow Cloud project running a Next.js app, deployed or in local dev
  • An Algolia account with an application and index (algolia.com)
  • A Cloudinary account with an upload preset configured (cloudinary.com)
  • A Webflow site token with cms:read scope (for syncing CMS content to Algolia)
  • Node.js 22.13.0 or higher locally, the minimum for Webflow CLI 2.0

Once these are ready, you can begin the full integration.

6 steps to add Algolia search and Cloudinary media optimization to a Webflow Cloud app

The pattern splits into two independent capabilities. Algolia handles search: a sync Route Handler indexes your Webflow CMS content, and a search proxy Route Handler keeps your Admin API key server-side while the browser performs search queries.

Cloudinary handles media: a URL utility generates optimized image URLs without additional requests, and an upload Route Handler accepts client-uploaded files and returns the Cloudinary public ID for storage.

1. Create an Algolia index and get your credentials

Log in to algolia.com and go to Search > Indices. Click Create Index and give it a name that matches your content (for example, webflow-blog-posts). Copy the index name; you'll need it as an environment variable.

Go to Settings > API Keys. You'll use two different keys:

  • Admin API Key: Grants full read/write access. Use this only in the CMS sync Route Handler. Never expose it to the browser.
  • Search-Only API Key: Read-only access for searching. This key is safe to use in a search proxy endpoint (though even here, keeping it server-side is good practice).

Add all credentials to .env.local and to Webflow Cloud's Settings > Environment Variables:

ALGOLIA_APP_ID=YOUR_APP_ID
ALGOLIA_ADMIN_API_KEY=your_admin_api_key
ALGOLIA_SEARCH_API_KEY=your_search_only_api_key
ALGOLIA_INDEX_NAME=webflow-blog-posts

None of these need a NEXT_PUBLIC_ prefix. All Algolia operations in this guide run in Route Handlers on the server side.

Then install the Algolia v5 JavaScript client:

npm install algoliasearch@5

The v5 client ships separate browser, Node.js, and worker builds, and selects between them by export condition. The worker build uses the Fetch API, which is the one that applies on Webflow Cloud.

2. Build an Algolia CMS sync Route Handler in Webflow Cloud

The sync endpoint fetches your Webflow CMS collection and indexes it to Algolia. You'll call this whenever CMS content changes: manually during development, or via a Webflow webhook on collection_item_created and collection_item_changed in production.

Here’s the snippet:

// app/api/algolia/sync/route.ts
import { algoliasearch } from 'algoliasearch'
import { NextResponse, type NextRequest } from 'next/server'

type WebflowCmsItem = {
  id: string
  fieldData: {
    name: string
    slug: string
    excerpt?: string
    'post-body'?: string
    [key: string]: unknown
  }
}

export async function POST(request: NextRequest) {
  // Protect the sync endpoint with a shared secret.
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.SYNC_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Fetch all CMS items from Webflow.
  const cmsResponse = await fetch(
    `https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items?limit=100`,
    {
      headers: {
        Authorization: `Bearer ${process.env.WEBFLOW_SITE_TOKEN}`,
      },
    }
  )

  if (!cmsResponse.ok) {
    return NextResponse.json(
      { error: 'Failed to fetch Webflow CMS' },
      { status: 502 }
    )
  }

  const { items } = await cmsResponse.json() as { items: WebflowCmsItem[] }

  // Map CMS items to Algolia objects.
  // objectID must be unique; use the Webflow item ID.
  const objects = items.map((item) => ({
    objectID: item.id,
    name: item.fieldData.name,
    slug: item.fieldData.slug,
    excerpt: item.fieldData.excerpt ?? '',
  }))

  // Index to Algolia using the admin key.
  const client = algoliasearch(
    process.env.ALGOLIA_APP_ID!,
    process.env.ALGOLIA_ADMIN_API_KEY!
  )

  await client.saveObjects({
    indexName: process.env.ALGOLIA_INDEX_NAME!,
    objects,
  })

  return NextResponse.json({ synced: objects.length })
}

Add SYNC_SECRET and WEBFLOW_COLLECTION_ID to your environment variables. The WEBFLOW_COLLECTION_ID is visible in the Webflow Designer at CMS > Collection Settings > Collection ID.

Trigger your first sync manually to confirm it works:

curl -X POST https://your-app.webflow.io/api/algolia/sync \
  -H "Authorization: Bearer your_sync_secret"

Check the Algolia dashboard under Search > Indices > [your-index] to confirm the records appeared.

3. Build an Algolia search proxy Route Handler in Webflow Cloud

The search proxy keeps your Algolia API key server-side and gives you a single endpoint to add rate limiting or query validation before the search runs.

The handler below reads the q and page query parameters, runs the search against your index with the search-only key, and returns only the fields the browser needs:

// app/api/search/route.ts
import { algoliasearch } from 'algoliasearch'
import { NextResponse, type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const query = searchParams.get('q') ?? ''
  const page = parseInt(searchParams.get('page') ?? '0', 10)

  if (!query.trim()) {
    return NextResponse.json({ hits: [], nbHits: 0, page: 0, nbPages: 0 })
  }

  const client = algoliasearch(
    process.env.ALGOLIA_APP_ID!,
    process.env.ALGOLIA_SEARCH_API_KEY!
  )

  const { results } = await client.search({
    requests: [
      {
        indexName: process.env.ALGOLIA_INDEX_NAME!,
        query,
        hitsPerPage: 10,
        page,
      },
    ],
  })

  // results[0] is a SearchResponse; cast to expose the fields we need.
  const result = results[0] as {
    hits: unknown[]
    nbHits: number
    page: number
    nbPages: number
  }

  return NextResponse.json({
    hits: result.hits,
    nbHits: result.nbHits,
    page: result.page,
    nbPages: result.nbPages,
  })
}

The search endpoint is now available at /api/search?q=your-query&page=0.

From any Client Component in your Webflow Cloud app:

// In a Client Component
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
const { hits, nbHits } = await res.json()

Because every query runs through your own endpoint, the Algolia key never reaches the browser, and you have a single place to add rate limiting or auth checks before a request hits Algolia. With indexing and search both working, the next steps move on to media.

4. Connect Cloudinary to your Webflow Cloud app

Log in to cloudinary.com. Your Cloud name is on the dashboard home page. Copy it.

To allow file uploads from your Webflow Cloud Route Handler, create an unsigned upload preset:

  1. Go to Settings > Upload > Upload presets.
  2. Click Add upload preset.
  3. Set Signing mode to Unsigned.
  4. Optionally, set a folder (e.g., webflow-cloud) to organize uploads.
  5. Save and copy the preset name.

Add both to your environment variables:

CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_UPLOAD_PRESET=webflow_unsigned
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name

CLOUDINARY_CLOUD_NAME is used in the Route Handler for uploads. NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME is safe to expose publicly: the cloud name is not a secret; it just identifies which Cloudinary account to fetch images from.

No package to install for the transformation utility. No package for the upload Route Handler either; both use plain fetch.

5. Optimize images with Cloudinary URL transformations in Webflow Cloud

Cloudinary applies transformations at the URL level. You construct the URL in a utility function; Cloudinary processes the transformation on the first request and serves it from the CDN cache on every subsequent request. No SDK, no runtime overhead.

Create a lib/cloudinary.ts utility:

// lib/cloudinary.ts

type TransformOptions = {
  width?: number
  height?: number
  crop?: 'fill' | 'fit' | 'scale' | 'thumb' | 'pad'
  quality?: 'auto' | number
  format?: 'auto' | 'webp' | 'avif' | 'jpg' | 'png'
  aspectRatio?: string
}

export function cloudinaryUrl(
  publicId: string,
  options: TransformOptions = {}
): string {
  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME!

  const transforms: string[] = []
  if (options.width) transforms.push(`w_${options.width}`)
  if (options.height) transforms.push(`h_${options.height}`)
  if (options.crop) transforms.push(`c_${options.crop}`)
  if (options.aspectRatio) transforms.push(`ar_${options.aspectRatio}`)
  if (options.quality) transforms.push(`q_${options.quality}`)
  if (options.format) transforms.push(`f_${options.format}`)

  const transformStr = transforms.length > 0 ? transforms.join(',') + '/' : ''
  return `https://res.cloudinary.com/${cloudName}/image/upload/${transformStr}${publicId}`
}

Use the utility anywhere in your Webflow Cloud app:

// A responsive hero image: 1200px wide, auto format, auto quality
const heroUrl = cloudinaryUrl('blog/hero-image', {
  width: 1200,
  format: 'auto',
  quality: 'auto',
})
// → https://res.cloudinary.com/your-cloud/image/upload/w_1200,f_auto,q_auto/blog/hero-image

// A square thumbnail with cropping
const thumbUrl = cloudinaryUrl('products/sneaker-01', {
  width: 400,
  height: 400,
  crop: 'fill',
  format: 'auto',
  quality: 'auto',
})
// → https://res.cloudinary.com/your-cloud/image/upload/w_400,h_400,c_fill,f_auto,q_auto/products/sneaker-01

f_auto is the most important single transformation on most projects: Cloudinary detects browser support and serves AVIF or WebP where available, falling back to JPEG. q_auto adjusts compression per image based on content type. Together they cut the image payload substantially with no visible change in quality.

The publicId is the identifier Cloudinary returns when an image is uploaded (covered in Step 6). For images already in Cloudinary, the public ID is visible in the Media Library.

6. Build a Cloudinary upload Route Handler in Webflow Cloud

The upload Route Handler accepts a file from a Client Component form, forwards it to Cloudinary, and returns the public ID and URL for storage in your app or Webflow CMS.

// app/api/upload/route.ts
import { NextResponse, type NextRequest } from 'next/server'

type CloudinaryUploadResult = {
  secure_url: string
  public_id: string
  width: number
  height: number
  format: string
  error?: { message: string }
}

export async function POST(request: NextRequest) {
  const formData = await request.formData()
  const file = formData.get('file') as File | null

  if (!file) {
    return NextResponse.json({ error: 'No file provided' }, { status: 400 })
  }

  // Enforce reasonable file size limits before sending to Cloudinary.
  const MAX_SIZE = 10 * 1024 * 1024 // 10 MB
  if (file.size > MAX_SIZE) {
    return NextResponse.json({ error: 'File too large (max 10 MB)' }, { status: 413 })
  }

  const cloudName = process.env.CLOUDINARY_CLOUD_NAME!
  const uploadPreset = process.env.CLOUDINARY_UPLOAD_PRESET!

  const uploadData = new FormData()
  uploadData.append('file', file)
  uploadData.append('upload_preset', uploadPreset)
  // Optionally set a folder for organization.
  uploadData.append('folder', 'webflow-cloud')

  const response = await fetch(
    `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
    {
      method: 'POST',
      body: uploadData,
    }
  )

  const result = await response.json() as CloudinaryUploadResult

  if (!response.ok) {
    return NextResponse.json(
      { error: result.error?.message ?? 'Upload failed' },
      { status: response.status }
    )
  }

  return NextResponse.json({
    url: result.secure_url,
    publicId: result.public_id,
    width: result.width,
    height: result.height,
    format: result.format,
  })
}

From a Client Component, the upload is a standard FormData fetch:

// In a Client Component
async function uploadImage(file: File) {
  const formData = new FormData()
  formData.append('file', file)

  const res = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  })

  const { publicId, url } = await res.json()
  // publicId can now be stored in Webflow CMS or your app's KV Store.
  // url is the direct Cloudinary URL for immediate use.
}

The publicId returned is what you pass to cloudinaryUrl() in Step 5. Once stored, you can generate any size or format from the same original without re-uploading.

What breaks Algolia search and Cloudinary media optimization in Webflow Cloud?

Most failures here trace back to a few predictable causes: missing searchable attributes on the Algolia index, an environment variable that does not match between local and deployed environments, an upload preset that is misnamed or still set to signed, or a browser that does not advertise WebP support.

Each item below pairs the symptom you will see with its cause and the fix.

Algolia returns no results after syncing CMS content

Open the Algolia dashboard at Search > Indices > [your-index] > Browse. If records are present but the search returns nothing, check the searchable attributes. Algolia indexes records by default but doesn't know which fields to search.

Go to Configuration > Searchable attributes and add the fields you want to search (e.g., name, excerpt). Save the configuration and rerun a search.

The sync Route Handler returns 401

The Authorization: Bearer [SYNC_SECRET] header value must exactly match what's in your Webflow Cloud environment variables. Check for trailing whitespace or character encoding issues when copying the secret.

Confirm the variable is set in Settings > Environment Variables (not just in .env.local) for the deployed environment.

Cloudinary upload returns 400 or "upload preset not found"

The upload preset name in CLOUDINARY_UPLOAD_PRESET must exactly match the preset name in Cloudinary Settings > Upload > Upload presets. Preset names are case-sensitive.

Also confirm the preset's Signing mode is set to Unsigned; signed presets require a timestamp and signature that this Route Handler doesn't generate.

Cloudinary images show the original format instead of WebP

The browser isn't sending an Accept header that includes the image/webp media type. This happens in older browsers and some headless fetch clients. For server-rendered images in Next.js, the <Image> component handles format negotiation automatically.

For client-fetched URLs, verify that f_auto is present in the transformation string.

Extend Algolia search and Cloudinary media optimization in your Webflow Cloud app

The setup in this guide covers the core flows: CMS indexing, search proxying, image transformation, and upload. Both extend cleanly from here.

For Algolia, the next step on most projects is adding faceted search: filters by category or date that narrow results without a full-text query. Algolia's faceting is configured in Configuration > Facets on the index, and the search Route Handler passes facetFilters in the request body alongside the query.

Explore Webflow + Algolia for a full breakdown of the integration approaches, from InstantSearch on the frontend to webhook-driven sync pipelines.

Explore Webflow + Cloudinary for the templates and URL parameters that control format and cropping once your media lives on Cloudinary's CDN.

Frequently asked questions

Is algoliasearch v5 compatible with Webflow Cloud's Workers runtime?

Yes. v5 publishes a worker build that uses the Fetch API, and bundlers targeting Cloudflare Workers resolve to it automatically. Cloudflare Workers is the runtime that powers Webflow Cloud. In v4 the client resolved to an XHR build in browsers and a Node.js http build in Node, so running it on Workers meant passing a fetch requester by hand. Use v5: npm install algoliasearch@5.

Can I expose my Algolia Search-Only API key in client-side code?

Algolia's Search-Only API key is scoped to read operations and cannot modify your index. Algolia's documentation explicitly permits client-side use of this key. That said, the proxy pattern from Step 3 is still preferable for Webflow Cloud because it lets you add rate limiting and query validation in one place without shipping that logic to the browser.

How large can Algolia records be?

Algolia's record size limit depends on your plan. The free Build plan caps each record at 10 KB, and paid plans raise that ceiling as high as 100 KB. Either way, avoid indexing the full body field for Webflow CMS blog posts with long content. Index name, slug, excerpt, and tags instead, and retrieve the full content from Webflow CMS once the user clicks a search result. On the free tier especially, this keeps each record under the 10 KB ceiling.

Do I need to set up a Cloudinary signed upload instead of unsigned?

Unsigned uploads are fine for most Webflow Cloud use cases. They require only an upload preset and impose no overhead for request signatures. If you need to control what's uploaded (e.g., restrict file types or apply transformations at upload time) without exposing those rules to the client, use signed uploads instead. Signed uploads require a server-side signature using api_secret and a timestamp. See the Cloudinary signed upload documentation.

Does Cloudinary cache transformed images?

Yes. Cloudinary generates the transformed version on the first request and caches it at their CDN edge for all subsequent requests. The transformation computation overhead only happens once per unique URL. This is why the URL-based transformation model is viable in production: after the first request, subsequent users get the CDN-cached version at full CDN speed with no Cloudinary processing time.

How do I automatically sync Webflow CMS changes to Algolia?

Register a Webflow webhook for collection_item_created and collection_item_changed pointing to your sync Route Handler URL. In the handler, instead of syncing the entire collection on every call, parse the webhook payload to retrieve the ID of the changed item, fetch that single item from the Webflow Data API, and call the client.saveObjects with just that record. This keeps the sync fast even for large collections.


Last Updated
July 26, 2026
Category

Related articles

How to link Webflow forms to HubSpot without losing your form design
How to link Webflow forms to HubSpot without losing your form design

How to link Webflow forms to HubSpot without losing your form design

How to link Webflow forms to HubSpot without losing your form design

Development
By
Colin Lateano
,
,
Read article
How to customize Webflow form notification emails for every form on your site
How to customize Webflow form notification emails for every form on your site

How to customize Webflow form notification emails for every form on your site

How to customize Webflow form notification emails for every form on your site

Development
By
Colin Lateano
,
,
Read article
How to build full-text search for Webflow CMS using Elasticsearch and a Route Handler
How to build full-text search for Webflow CMS using Elasticsearch and a Route Handler

How to build full-text search for Webflow CMS using Elasticsearch and a Route Handler

How to build full-text search for Webflow CMS using Elasticsearch and a Route Handler

Development
By
Colin Lateano
,
,
Read article
How to add a Calendly popup modal to Webflow and keep visitors on-site
How to add a Calendly popup modal to Webflow and keep visitors on-site

How to add a Calendly popup modal to Webflow and keep visitors on-site

How to add a Calendly popup modal to Webflow and keep visitors on-site

Development
By
Colin Lateano
,
,
Read article

verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.