How to build a Gemini content assistant that belongs inside your Webflow CMS

Learn how to build a Gemini content assistant on Webflow Cloud that streams drafts and returns CMS-ready JSON.

How to build a Gemini content assistant that belongs inside your Webflow CMS

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

A content assistant that sits next to your CMS and hands back fields you can paste without reformatting changes how editors work in Webflow.

Content editors working within Webflow CMS lose a surprising amount of time to the same friction points. They stare at a blank field, reformat a draft into the fields their collection expects, or toggle between an AI chat window and the Webflow dashboard to copy and paste content.

A Gemini-powered content assistant built on Webflow Cloud eliminates all three. It lives inside the same environment as your site, understands the shape of your CMS collection, and can return either a streaming draft for open-ended writing or a structured JSON object that maps directly to your Webflow CMS fields.

When Gemini outputs a JSON object already formatted for Webflow CMS, the copy-paste-reformat loop disappears entirely.

In this guide, we’ll build that assistant on Webflow Cloud with Next.js and the Gemini API, then add two modes: a streaming draft mode for open-ended writing and a CMS mode that returns fields you can paste straight into your collection.

What do you need to build a Gemini content assistant in Webflow Cloud?

You need a Webflow Cloud Next.js project and a Gemini API key from Google AI Studio. The @google/genai SDK talks to Gemini over fetch, so it runs on Webflow Cloud’s Workers runtime with no Node.js-specific dependencies. Do not add export const runtime = 'edge' to your routes: the OpenNext Cloudflare adapter Webflow Cloud uses does not support it.

Here’s the list of requirements:

  • A Webflow Cloud project running a Next.js app, deployed or in local dev
  • A Gemini API key (free tier available at aistudio.google.com/apikey)
  • Node.js 22.13.0 or higher locally, the floor set by Webflow CLI 2.x; the @google/genai SDK itself only requires Node 20
  • A basic familiarity with Webflow CMS collection field names (you'll reference them in the system prompt)

No third-party AI proxy, serverless function platform, or separate hosting. Everything runs in one Webflow Cloud app.

5 steps to build a Gemini content assistant for Webflow

The architecture splits into two pieces:

  • A streaming Route Handler that calls Gemini and returns output as a text stream
  • A Client Component that sends the editor's prompt and renders the response word by word

A second, non-streaming endpoint handles "CMS mode": it outputs a JSON object that maps directly to Webflow CMS fields.

I use two modes on every project. Streaming draft mode for open-ended exploration, JSON mode for structured CMS-ready output. Writers love streaming because it feels live. I love JSON mode because it removes the reformatting step from the publishing workflow.

1. Get a Gemini API key and install the SDK

Log in or sign up at Google AI Studio. Click Get API key > Create API key. Copy the key and store it somewhere safe.

Install the Google Gen AI SDK in your Webflow Cloud project:

npm install @google/genai

Add your API key to .env.local and to Webflow Cloud's environment dashboard under Settings > Environment Variables:

GEMINI_API_KEY=your_gemini_api_key_here

Do not prefix this with NEXT_PUBLIC_. Any variable prefixed with NEXT_PUBLIC_ is included in the browser JavaScript bundle. A public Gemini API key lets anyone run generation requests billed to your account.

2. Build the Gemini streaming Route Handler in Webflow Cloud

The streaming endpoint receives a prompt from the editor, calls Gemini with a system prompt that shapes the tone and output format, then streams the response back as plain text. The for await loop on the Gemini stream feeds chunks into a ReadableStream that Next.js sends directly to the browser.

Here’s the snippet:

// app/api/assist/stream/route.ts
import { GoogleGenAI } from '@google/genai'
import { type NextRequest } from 'next/server'

const SYSTEM_PROMPT = `You are a content editor for a Webflow site.
Your writing is clear, specific, and aimed at practitioners.
Avoid filler phrases, excessive hedging, and generic transitions.
Match the requested tone. Output plain prose without markdown headers
unless the user explicitly asks for structured content.`

export async function POST(request: NextRequest) {
  const { topic, tone } = await request.json() as {
    topic: string
    tone: 'professional' | 'conversational' | 'technical'
  }

  const prompt = `Write a first draft about: ${topic}\nTone: ${tone}`

  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! })

  const stream = new ReadableStream({
    async start(controller) {
      try {
        const responseStream = await ai.models.generateContentStream({
          model: 'gemini-3.5-flash',
          contents: prompt,
          config: {
            systemInstruction: SYSTEM_PROMPT,
            maxOutputTokens: 1024,
          },
        })

        for await (const chunk of responseStream) {
          if (chunk.text) {
            controller.enqueue(new TextEncoder().encode(chunk.text))
          }
        }
      } catch (err) {
        controller.error(err)
        return
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
  })
}

With this handler in place, a POST to /api/assist/stream returns Gemini's output as a live text stream rather than a single blob. The browser reads it chunk by chunk, which is what produces the typewriter effect in the UI you'll build in step 4.

Error handling matters here. Calling controller.error() when Gemini throws surfaces the failure to the browser, instead of closing the stream silently, which would be indistinguishable from an empty successful response.

3. Build the Gemini CMS-mode Route Handler in Webflow Cloud

The CMS mode endpoint uses Gemini's structured output feature (responseMimeType: 'application/json' with a responseSchema) to return a JSON object that maps exactly to your Webflow CMS fields. The editor gets a result they can paste directly, no reformatting required.

Here's the CMS-mode route handler that pins the output to your collection schema:

// app/api/assist/cms/route.ts
import { GoogleGenAI } from '@google/genai'
import { NextResponse, type NextRequest } from 'next/server'

// Adjust these field names to match your Webflow CMS collection.
const CMS_SCHEMA = {
  type: 'object',
  properties: {
    name: { type: 'string', description: 'CMS item name, max 80 characters' },
    slug: { type: 'string', description: 'URL slug, lowercase, hyphenated' },
    excerpt: { type: 'string', description: 'Short summary, max 160 characters' },
    body: { type: 'string', description: 'Full article body in plain text' },
    tags: {
      type: 'array',
      items: { type: 'string' },
      description: 'Up to 5 relevant tags',
    },
  },
  required: ['name', 'slug', 'excerpt', 'body'],
}

export async function POST(request: NextRequest) {
  const { topic, tone } = await request.json() as {
    topic: string
    tone: string
  }

  const prompt = `Create Webflow CMS content about: ${topic}\nTone: ${tone}`

  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! })

  const response = await ai.models.generateContent({
    model: 'gemini-3.5-flash',
    contents: prompt,
    config: {
      systemInstruction: `You are a Webflow content editor.
        Output only valid JSON matching the provided schema.
        The body field should be 300-500 words of plain text.`,
      responseMimeType: 'application/json',
      responseSchema: CMS_SCHEMA,
      maxOutputTokens: 2048,
      temperature: 0.5,
    },
  })

  const json = JSON.parse(response.text ?? '{}')
  return NextResponse.json(json)
}

One note on responseSchema: Google's API reference now flags it as deprecated, though it is still fully documented, has no announced removal date, and must be paired with a compatible responseMimeType. Treat it as something to revisit, not something to rewrite today.

In my experience, the system prompt does more for output quality than anything else in this build. I spend more time tuning the system instruction (adjusting word count targets, adding field descriptions, specifying what "plain text" means for a given CMS setup) than writing any of the application code.

4. Build the Gemini content assistant UI

The Client Component manages the form state and the streaming reader. When the editor submits a prompt, it opens a ReadableStream from the fetch response and appends each decoded chunk to a state variable, producing the typewriter effect.

Here's the client component that manages the form state and the streaming reader:

// app/content-assistant/ContentAssistant.tsx
'use client'

import { useState } from 'react'

type Mode = 'draft' | 'cms'
type Tone = 'professional' | 'conversational' | 'technical'

type CmsOutput = {
  name: string
  slug: string
  excerpt: string
  body: string
  tags?: string[]
}

export default function ContentAssistant() {
  const [topic, setTopic] = useState('')
  const [tone, setTone] = useState<Tone>('professional')
  const [mode, setMode] = useState<Mode>('draft')
  const [output, setOutput] = useState('')
  const [cmsOutput, setCmsOutput] = useState<CmsOutput | null>(null)
  const [loading, setLoading] = useState(false)

  async function generate() {
    if (!topic.trim() || loading) return
    setLoading(true)
    setOutput('')
    setCmsOutput(null)

    try {
      if (mode === 'draft') {
        const res = await fetch('/api/assist/stream', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ topic, tone }),
        })

        if (!res.ok) throw new Error('Request failed')

        const reader = res.body!.getReader()
        const decoder = new TextDecoder()

        while (true) {
          const { done, value } = await reader.read()
          if (done) break
          setOutput((prev) => prev + decoder.decode(value, { stream: true }))
        }
      } else {
        const res = await fetch('/api/assist/cms', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ topic, tone }),
        })
        const data = await res.json() as CmsOutput
        setCmsOutput(data)
      }
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="content-assistant">
      <h1>Gemini Content Assistant</h1>

      <div className="assistant-controls">
        <div className="mode-toggle">
          <button
            className={mode === 'draft' ? 'active' : ''}
            onClick={() => setMode('draft')}
          >
            Draft mode
          </button>
          <button
            className={mode === 'cms' ? 'active' : ''}
            onClick={() => setMode('cms')}
          >
            CMS mode
          </button>
        </div>

        <textarea
          value={topic}
          onChange={(e) => setTopic(e.target.value)}
          placeholder="Describe what you want to write about..."
          rows={3}
          className="assistant-input"
        />

        <select
          value={tone}
          onChange={(e) => setTone(e.target.value as Tone)}
          className="assistant-tone"
        >
          <option value="professional">Professional</option>
          <option value="conversational">Conversational</option>
          <option value="technical">Technical</option>
        </select>

        <button
          onClick={generate}
          disabled={loading || !topic.trim()}
          className="assistant-submit"
        >
          {loading ? 'Generating...' : 'Generate'}
        </button>
      </div>

      {mode === 'draft' && output && (
        <div className="assistant-output">
          <pre>{output}</pre>
          <button onClick={() => navigator.clipboard.writeText(output)}>
            Copy to clipboard
          </button>
        </div>
      )}

      {mode === 'cms' && cmsOutput && (
        <div className="assistant-cms-output">
          <div className="cms-field">
            <label>Name</label>
            <p>{cmsOutput.name}</p>
          </div>
          <div className="cms-field">
            <label>Slug</label>
            <code>{cmsOutput.slug}</code>
          </div>
          <div className="cms-field">
            <label>Excerpt</label>
            <p>{cmsOutput.excerpt}</p>
          </div>
          {cmsOutput.tags && (
            <div className="cms-field">
              <label>Tags</label>
              <p>{cmsOutput.tags.join(', ')}</p>
            </div>
          )}
          <div className="cms-field">
            <label>Body</label>
            <pre>{cmsOutput.body}</pre>
          </div>
          <button
            onClick={() => navigator.clipboard.writeText(JSON.stringify(cmsOutput, null, 2))}
          >
            Copy as JSON
          </button>
        </div>
      )}
    </div>
  )
}

This component drives both modes from one form. In draft mode, it reads the response stream and appends each chunk to the state, so text appears as it is generated. In CMS mode, it waits for the full JSON object, then renders each field with its own copy control. Both paths share the same topic and tone inputs, so switching modes never makes the editor re-enter anything.

5. Add the Gemini content assistant to a Webflow Cloud page

Create the page that serves the assistant. Because ContentAssistant is a Client Component, the page itself is a Server Component wrapper:

// app/content-assistant/page.tsx
import ContentAssistant from './ContentAssistant'

export default function ContentAssistantPage() {
  return (
    <main className="assistant-page">
      <ContentAssistant />
    </main>
  )
}

The assistant is now live at /content-assistant in your Webflow Cloud app. If the app is deployed as part of a Webflow site, mount it at the path you configure in Site Settings > Webflow Cloud. Access can be gated with any auth middleware; see the Supabase Auth guide to restrict the assistant to logged-in team members.

What breaks a Gemini content assistant in Webflow?

Most failures with a Gemini content assistant trace back to three things: an output-token limit set too low, malformed JSON in CMS mode, and an API key that works locally but not in production. Each one surfaces at a predictable point, and each has a fast fix once you know the cause.

Here is each failure pattern.

Streaming response is empty or stops mid-sentence

The most common cause is the maxOutputTokens limit being too low for the requested content. The example above sets it to 1024 tokens, which fits a 300-400 word draft.

For longer content, raise maxOutputTokens (Gemini 3.5 Flash supports up to 65,536 output tokens). Also check that the finally { controller.close() } block is present; without it, a Gemini API error leaves the stream open, and the UI hangs.

CMS JSON output has extra text or markdown around the JSON

This happens when responseMimeType is set, but the model adds an explanation. Always pass responseMimeType: 'application/json' alongside responseSchema in the config; without the schema, the model may wrap the JSON in markdown code fences.

If you still see extra text, add "Output only raw JSON. No explanation, no code fences." to the system instruction.

The Gemini API key is being rejected in production

Check that GEMINI_API_KEY is set in Webflow Cloud's environment variables at Settings > Environment Variables, not just in .env.local. Environment variables set locally don't carry over to production.

Also verify the key has not hit the free tier rate limit; the Gemini free tier has per-minute generation limits that can be exceeded quickly during testing.

Extend your Gemini content assistant for Webflow CMS workflows

The assistant in this guide covers the two patterns I use on every project: streaming drafts and structured CMS output. Both extend cleanly from here.

For multi-turn revision, swap the generateContent call for ai.chats.create() and maintain the chat object in a React ref across renders. The editor can then ask Gemini to rewrite a specific paragraph, change the tone, or shorten the excerpt without losing the original context.

Explore Webflow + Gemini for the connection options, supported capabilities, and setup notes that sit underneath the build in this guide.

For Webflow Cloud deployment and environment configuration, see the Webflow Cloud documentation.

Frequently asked questions

Which Gemini model should I use?

gemini-3.5-flash is the right default for most content assistant use cases. It's fast enough for real-time streaming, handles long outputs cleanly, and supports structured JSON output via the responseMimeType parameter. For content that needs deeper reasoning or a larger context window, step up to Google's Pro tier. Gemini 3.1 Pro (gemini-3.1-pro-preview) is the Pro-class model callable through the API today, and Gemini 3.5 Pro was announced at Google I/O 2026 but is not yet callable through the API. Pro trades speed for depth of reasoning, so reach for it only when Flash falls short.

Does the @google/genai SDK run on Webflow Cloud?

Yes. The @google/genai SDK uses the Fetch API for HTTP requests, which is available in the Cloudflare Workers runtime that Webflow Cloud runs on. Do not add export const runtime = 'edge' to the route: Next.js’s edge runtime is not supported by the OpenNext Cloudflare adapter, and leaving the directive in place fails the build.

Can I connect the assistant directly to the Webflow CMS to publish content?

Not directly from the Route Handler, but the path is straightforward. After the CMS mode endpoint returns a JSON object, make a POST request to the Webflow Data API at https://api.webflow.com/v2/collections/{collection_id}/items using a site token with CMS:write scope. The JSON from Gemini maps to the fieldData object in the Webflow API request body.

What does the Gemini free tier cover?

The Gemini API free tier includes rate-limited access to gemini-3.5-flash with no billing required. For a content assistant used by a small team, the free tier is typically sufficient for testing and light production use. High-volume use cases require a paid tier. Rate limits vary by usage tier and are shown per project in AI Studio, so see how limits are assigned and per-token pricing for current figures.

How do I prevent editors from sending arbitrary prompts to Gemini?

Two approaches work well together: validate and sanitize the topic field in the Route Handler before it reaches Gemini (enforce a character limit, strip HTML tags), and scope the system instruction to define what the model should and shouldn't do. For teams where content scope is tightly controlled, adding a dropdown of predefined content types (blog post, product description, announcement) replaces the free-text topic field and keeps generation within expected bounds.


Last Updated
August 8, 2026
Category

Related articles

How to add Crisp live chat to a Webflow Cloud app the right way
How to add Crisp live chat to a Webflow Cloud app the right way

How to add Crisp live chat to a Webflow Cloud app the right way

How to add Crisp live chat to a Webflow Cloud app the right way

Guides
By
Ismail Ajagbe
,
,
Read article
How to add reCAPTCHA spam protection to Webflow forms and block automated bots
How to add reCAPTCHA spam protection to Webflow forms and block automated bots

How to add reCAPTCHA spam protection to Webflow forms and block automated bots

How to add reCAPTCHA spam protection to Webflow forms and block automated bots

Development
By
Colin Lateano
,
,
Read article
How to build a serverless app with Neon Postgres and Webflow Cloud
How to build a serverless app with Neon Postgres and Webflow Cloud

How to build a serverless app with Neon Postgres and Webflow Cloud

How to build a serverless app with Neon Postgres and Webflow Cloud

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.