A Webflow Cloud Route Handler turns Drive files into image URLs on your own domain that the Webflow CMS can import straight into an Image field.
Google Drive stopped serving images through its old uc?export=view links in 2024, so a Drive share link pasted into a CMS field now shows a broken image.
If your assets and campaign imagery still live in Drive, you can build a small Next.js app on Webflow Cloud with one Route Handler that fetches an allow-listed Drive file through the Drive API and serves its bytes from your own domain.
What do you need to get Google Drive image URLs in Webflow?
You need six things:
- Webflow site: A site with a CMS collection that has an Image field, plus a site token with the
cms:writescope for the import. - Node.js 22 and npm: Webflow Cloud builds on Node.js 22 or later, and the bring-your-own-app page says: "Currently, Webflow Cloud supports only the npm package manager."
- Next.js 15 or higher: The version floor Webflow Cloud supports for Next.js apps.
- Google Cloud project: A project with the Google Drive API enabled, where you can create an API key.
- Drive files you can share: Images shared as Anyone with the link, each under 4 MB, because that is the largest image the Webflow Data API accepts.
- Deploy access: Someone with permission to deploy to the site's Webflow Cloud project; agree who that is before an agency handoff.
With those in place, the build is one Route Handler plus an import request per image.
6 steps to get Google Drive image URLs in Webflow Cloud
The build shares the files and creates a key, writes a Route Handler that proxies allow-listed Drive files, deploys it under /assets, and imports each file into the CMS through the Data API.
1. Share the Drive files and create a Google API key
In each image's Share dialog in Drive, set General access to Anyone with the link. The route reads files with a Google Cloud API key, which Google describes as a way to access publicly available data anonymously, so it can only read files shared this way.
That suits imagery bound for a public website. If your Workspace admin blocks link sharing, the route needs a service account instead, which is beyond this guide.
Copy each file's ID from its share link: it is the long string between /d/ and /view. Then, in the Google Cloud console for your project, enable the Google Drive API and create an API key restricted to the Drive API, so a leaked key cannot call anything else.
At the end of this step you have an API key and a list of file IDs, and each file opens in a private browser window without signing in.
2. Create the Next.js app and its environment variables
Scaffold a Next.js 15 app with the App Router and TypeScript, and leave the runtime setting alone. Run npx create-next-app@15 drive-images, accept the App Router and TypeScript prompts, and open the project in an editor.
Webflow's configuration page says "You don't need to add an adapter, a base path, or an output mode," so next.config stays at its default. Do not add export const runtime = 'edge' to any route, even though the bring-your-own-app page still tells you to add it to API routes.
Why edge runtime breaks Next.js deploys
Webflow Cloud deploys on Cloudflare Workers, which is an edge platform, but the Next.js edge runtime target is a separate setting that the OpenNext Cloudflare adapter behind Webflow Cloud does not support, so the directive breaks the build.
Add three variables to a .env.local file in the project root:
- GOOGLE_API_KEY: The restricted key from step 1; mark it secret in Webflow Cloud.
- DRIVE_ALLOWED_FILE_IDS: The comma-separated file IDs you intend to serve. Any other ID returns 404, so the route never acts as an open proxy.
- IMPORT_TOKEN: A long random string the route requires on every request; mark it secret. Only Webflow's importer and your own tests ever need to send it.
When you deploy, the same three go into the app's Webflow Cloud environment. Webflow's environment variables page states that "Both secret and non-secret environment variables are available to your application's build process and to the deployed application at runtime," and secret values are redacted from build logs.
Run npm run dev and open the local URL. The default Next.js welcome page confirms the scaffold works before any Drive code exists.
3. Write the Route Handler that proxies allow-listed Drive files
Create the handler at app/api/drive/[fileId]/route.ts. On each request, it checks the import token and the allow-list, fetches the file through the Drive API's files.get method with alt=media, which returns the file's contents, and streams the bytes back with Drive's content type.
The complete handler fits in one file:
// app/api/drive/[fileId]/route.ts
const MAX_BYTES = 4 * 1024 * 1024;
export async function GET(
request: Request,
{ params }: { params: Promise<{ fileId: string }> },
) {
const { fileId } = await params;
const apiKey = process.env.GOOGLE_API_KEY;
const importToken = process.env.IMPORT_TOKEN;
if (!apiKey || !importToken) {
return new Response('Drive configuration is missing', { status: 500 });
}
const token = new URL(request.url).searchParams.get('token') ?? '';
const allowedIds = new Set(
(process.env.DRIVE_ALLOWED_FILE_IDS ?? '')
.split(',')
.map((id) => id.trim())
.filter(Boolean),
);
if (token !== importToken || !allowedIds.has(fileId)) {
return new Response('Not found', { status: 404 });
}
const upstreamUrl =
`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}` +
`?alt=media&key=${encodeURIComponent(apiKey)}`;
let upstream: Response;
try {
upstream = await fetch(upstreamUrl, { signal: AbortSignal.timeout(10000) });
} catch {
return new Response('Upstream fetch failed', { status: 502 });
}
if (!upstream.ok || !upstream.body) {
return new Response(`Drive returned ${upstream.status}`, { status: 502 });
}
const contentType = upstream.headers.get('content-type') ?? '';
if (!contentType.startsWith('image/')) {
return new Response('Not an image', { status: 415 });
}
const size = Number(upstream.headers.get('content-length') ?? 0);
if (size > MAX_BYTES) {
return new Response('Image is larger than 4 MB', { status: 413 });
}
return new Response(upstream.body, {
status: 200,
headers: { 'Content-Type': contentType },
});
}
The allow-list and the token together are the security boundary. Without the token, the route answers 404 for every file, so nobody can use it to spend your Google quota or pull files you did not list.
The startsWith('image/') check stops a Drive error page from passing through as an image, and the size check returns a clear 413 before Webflow's 4 MB limit turns into a silently missing image later. The body streams straight through without buffering.
Test locally with curl -I "http://localhost:3000/api/drive/ONE_OF_YOUR_IDS?token=YOUR_IMPORT_TOKEN". A correct setup returns a 200 with an image/ content type, and the same request without the token returns 404 before any Drive request is made.
4. Deploy the app and mount it at /assets
Deploy the project to Webflow Cloud following the bring-your-own-app steps, and set the app's mount path to /assets so the URLs read as asset addresses.
Add the three variables to the app's Webflow Cloud environment, with GOOGLE_API_KEY and IMPORT_TOKEN marked secret, then push a commit so a new deployment picks them up; variable changes apply only to a new deployment.
With the mount path saved, every route in the app sits under /assets, so each image's address is your domain, then /assets/api/drive/, then the file ID and the token. Request one with curl against the deployed domain and confirm a 200 with an image/ content type before importing anything.
The route serves its own app paths, so nothing in the browser needs NEXT_PUBLIC_BASE_PATH here. The mount path only has to appear in the URLs you send to the importer in the next step.
5. Import each image into a CMS Image field through the Data API
A CMS Image field written through the Data API takes an object with a URL and an optional alt. Webflow downloads the image from that URL and stores its own copy, so the URL must be publicly reachable and the file must be 4 MB or smaller.
Your route satisfies both requirements, and once the import finishes, the item no longer depends on Drive or the route.
Create items with the Create Items endpoint, replacing the placeholder values with your own:
curl -X POST "https://api.webflow.com/v2/collections/$COLLECTION_ID/items/insert?skipInvalidFiles=false" \
-H "Authorization: Bearer $WEBFLOW_SITE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [{
"fieldData": {
"name": "Spring campaign hero",
"slug": "spring-campaign-hero",
"drive-file-id": "FILE_ID",
"hero-image": {
"url": "https://yourdomain.com/assets/api/drive/FILE_ID?token=YOUR_IMPORT_TOKEN",
"alt": "Product shot on a green background"
}
}
}]
}'
The skipInvalidFiles=false parameter matters. By default, the endpoint skips any file it cannot import and creates the item anyway, so a wrong token or an oversized file would leave an item with an empty image and no error.
With the parameter set to false, the whole request fails instead, and the response tells you which file was the problem. The drive-file-id value goes into a Plain text field I add to the collection, which the duplicate check in the FAQ relies on.
Items are created as drafts. Open the collection in the CMS, check the image on each new item, and publish them when they look right.
6. Bind the Image field and check the published page
In the Designer, bind an Image element on the Collection template or inside a Collection List to the hero-image field, the same way you would for an image uploaded by hand. Because Webflow imported its own copy, the image now serves from Webflow's asset hosting with responsive sizes, and it keeps working even if the Drive file is later moved.
Adding a new image means appending its ID to DRIVE_ALLOWED_FILE_IDS, pushing a commit to redeploy, and running the import request for it. If editors add files weekly, move the allow-list to a store they can edit without a deploy.
Publish the page and load it. Open the image in a new tab: the address should be on Webflow's asset host rather than /assets/api/drive/, which confirms the import created Webflow's own copy.
What causes Google Drive image URLs to fail on Webflow Cloud?
Build failures almost always trace back to the edge directive. A status code from the route points at the request URL or at Drive.
Webflow Cloud build fails on an API route that runs under npm run dev
Cause: The OpenNext Cloudflare adapter that Webflow Cloud uses to deploy Next.js does not support the Next.js edge runtime target, so any route carrying export const runtime = 'edge' breaks the build.
The directive raises no error under npm run dev because the local dev server does not build through the adapter. Developers usually add it because the bring-your-own-app page describes the platform as "Webflow Cloud deploys your app using the Edge runtime, enabling fast, globally distributed hosting" and still recommends the directive for API routes.
Fix: Remove the runtime export from every route and redeploy. Search the whole project for runtime = 'edge' first, since a single remaining instance keeps the build red.
The import creates the item, but the request fails, or the image is empty
Cause: Webflow could not fetch a valid image from the URL. The route returns 404 when the token is missing or wrong, or when the ID is not on the allow-list, and 413 when the file is over 4 MB.
A URL without the /assets mount path never reaches the app at all. With skipInvalidFiles left at its default, each of these produces an item with an empty image field and no error.
Fix: Request the exact URL from the import body with curl -I and read the status. Correct whichever part of the URL is wrong, compress any file over 4 MB, and run the import again with skipInvalidFiles=false so a bad file stops the request instead of passing silently.
Route returns 502 or 415 instead of an image
Cause: A 502 means Drive rejected the request or never answered. The usual causes are a file not shared as Anyone with the link, an API key without the Drive API enabled or restricted to a different API, or a deleted key. A 415 means Drive answered with something other than an image, for example, a PDF or a Google Docs file.
Fix: Request the file from Drive directly with curl -I "https://www.googleapis.com/drive/v3/files/FILE_ID?alt=media&key=YOUR_KEY" and read the status and Content-Type.
Fix the sharing settings or the key's restrictions until that request returns an image, and remove non-image IDs from the allow-list. If only the deployed route fails, the Webflow Cloud environment differs from .env.local; update it and push a commit to redeploy.
What you can build next with Google Drive and Webflow
The next step is a sync script that lists a Drive folder through the Drive API, compares the file IDs with the drive-file-id values already in the collection, and imports only the new ones. Once the files your team already manages in Drive reach the CMS without anyone re-uploading them, the same route pattern extends to any file source that needs a stable public URL.
For moving content the other way, out of Webflow, the blog export guide uses the same Data API.
For anything beyond a single proxy route, Webflow's developer docs cover updating existing items, including replacing an image.
Frequently asked questions
Can I use a Google Drive share link directly as an image src?
No. Since early 2024, Drive's old uc?export=view links return a 403 when used in an image tag, and the thumbnail links in the Drive API expire within hours. Google's own field reference says thumbnail links are not intended for direct use in web applications and suggests a proxy server, which is what this build is.
What happens when a designer replaces a file in Drive?
Nothing changes on the site, because Webflow serves the copy it imported. To pull the new version, update the item's Image field with the route URL again, adding a changed v parameter such as &v=2 so the URL differs from the last import and Webflow downloads it afresh.
Can I allow-list a whole Drive folder instead of individual file IDs?
Not with this handler, which checks each request against an exact set of file IDs. A folder-driven list would mean querying Drive for the folder's contents and storing the result somewhere editors can change without a redeploy.
How do I avoid duplicate CMS items if an import runs twice?
Store the Drive file ID in its own Plain text field, as the import request does with drive-file-id. A script then matches existing items on that field and updates the match instead of creating a new item, so a second run changes nothing. Each request counts against Webflow's rate limit of 60 requests per minute on Starter and Basic sites, so batch items into one insert request where you can.





