Short answer
For the files in Webflow’s Assets panel, the free Asset Exporter app now exports them as one ZIP. For what every live page actually serves, on Webflow or any site with a sitemap, the free Python script below walks every page and saves every image it finds.
A client asked me to back up every image on their Webflow site. Straightforward request. Then I went looking for the button.
There isn’t one built in. The Asset Manager lets you browse your images and download them one at a time. On a site with four hundred assets, that’s an afternoon of clicking.
Why the obvious workarounds fall short
Code export is the one people suggest first. It needs a paid Workspace plan, and the ZIP leaves out CMS content altogether, so every image that lives in a Collection item is missing. On a content-heavy site that’s most of them.
Browser extensions that grab images work on one page at a time. Same problem as the Asset Manager, different interface.
Right-click, save as, four hundred times. Technically a solution.
The Asset Exporter app is the exception, and newer than the first version of this post. It’s free, made by Amply, in the Webflow Marketplace, and it exports your site’s assets as a single ZIP in one click. If what you want is a backup of what you uploaded, I’d try it first. Its listing doesn’t say whether images inside Collection items are included, so check that on your own project. The script below is for when you want exactly what each live page serves, page by page, or the site isn’t on Webflow at all.
What you actually want is something that walks the whole site once and takes everything. Your sitemap already lists every page, so that’s the obvious thing to walk.
Figure
The script walks your sitemap, so it finds every page, not just the ones you remember
- 01Sitemap index
- 02Child sitemaps
- 03Every page
- 04Every image URL
- 05site-images/ folder
Can you download all images from Webflow at once?
Not with a built-in button. Webflow’s code export includes every image in the Assets panel but needs a paid Workspace plan, and the free Asset Exporter app zips your project’s assets from inside the Designer. Code export leaves out CMS content, and the app’s listing doesn’t say whether it includes CMS images.
Each method catches a different set of images, so the right one depends on whether you want what you uploaded or what the live site shows.
| Method | Bulk download? | Images in CMS items? | Assets no page uses? | Works off Webflow? | Needs Python? | Plan needed |
|---|---|---|---|---|---|---|
| Asset Manager (Assets panel) | No bulk download in Webflow’s documentation | No, they aren’t in the Assets panel | Yes | No | No | Not stated |
| Code export | Yes, one ZIP | No, CMS content is excluded | Yes, every Assets panel image | No | No | Paid Workspace plan |
| Asset Exporter app | Yes, one ZIP | Not stated | Yes, “all of your Webflow project’s assets” | No, it runs in the Designer | No | Not stated |
| Browser extension | One page at a time | Only those on the open page | No | Yes | No | None |
| This script | Yes, every page in the sitemap | Yes, if a published page shows them | No | Yes, any site with an XML sitemap | Yes | None |
Sources: Webflow’s help pages on code export and the Assets panel, and the Asset Exporter’s Marketplace listing linked above, checked 5 October 2026. “Not stated” means the source doesn’t say.
Webflow’s Assets panel page covers uploading, organising, compressing and deleting assets in bulk, and describes no way to download them in bulk.
Does Webflow code export include CMS images?
No. Webflow’s code export documentation says CMS content, including Collection items and Collection lists, isn’t exported, and Collection lists show their empty state. The export’s images folder holds what’s in the Assets panel, and Webflow describes that panel as being for assets that aren’t added to a CMS Collection item.
On a site built around the CMS, that gap can be most of the pictures: every blog post’s main image, every image in rich text, every portfolio gallery. The script doesn’t care where an image was uploaded.
It reads the published HTML of every page in your sitemap, so a Collection page’s images are caught like any other, provided the page is published and listed in the sitemap.
If you’re relying on the Asset Exporter for a backup, I’d check whether its ZIP includes your CMS images on your own project first, because its listing doesn’t say.
The script
Reads your sitemap, follows it to every page, finds every image URL in the HTML, and downloads each one once.
"""
Download every image a website serves, using its sitemap.
Works on any site with an XML sitemap - Webflow, WordPress, Shopify,
Squarespace, static builds. Handles sitemap index files, keeps the URL
path in the filename so two 'hero.jpg' files can't overwrite each other,
and skips anything already downloaded so you can re-run it safely.
pip install requests
python download-images.py https://www.example.com/sitemap.xml
"""
import os
import re
import sys
import xml.etree.ElementTree as ET
from urllib.parse import urlparse, unquote
import requests
OUT = "site-images"
UA = {"User-Agent": "Mozilla/5.0 (compatible; image-backup/1.0)"}
NS = {"ns": "http://www.sitemaps.org/schemas/sitemap/0.9"}
IMG = re.compile(r'https?://[^"\'\s)\\]+?\.(?:jpg|jpeg|png|gif|webp|svg|avif)', re.I)
def fetch(url):
r = requests.get(url, headers=UA, timeout=20)
r.raise_for_status()
return r
def page_urls(sitemap_url, seen_maps=None):
"""Yield every page URL, following sitemap index files one level deep."""
seen_maps = seen_maps or set()
if sitemap_url in seen_maps:
return
seen_maps.add(sitemap_url)
root = ET.fromstring(fetch(sitemap_url).content)
# A sitemap index points at more sitemaps rather than at pages.
children = [el.text for el in root.findall(".//ns:sitemap/ns:loc", NS)]
if children:
print(f"Sitemap index: {len(children)} child sitemaps")
for child in children:
yield from page_urls(child, seen_maps)
return
for loc in root.findall(".//ns:loc", NS):
if loc.text:
yield loc.text.strip()
def safe_name(img_url):
"""Keep the path, not just the basename - two pages often both ship
a 'hero.jpg', and basename alone silently loses one of them."""
path = unquote(urlparse(img_url).path).lstrip("/")
name = re.sub(r"[^A-Za-z0-9._/-]", "_", path).replace("/", "__")
return name[-180:] or "image"
def main():
if len(sys.argv) < 2:
sys.exit("Usage: python download-images.py https://example.com/sitemap.xml")
os.makedirs(OUT, exist_ok=True)
seen, saved, failed = set(), 0, 0
for url in page_urls(sys.argv[1]):
print(f"Scanning: {url}")
try:
html = fetch(url).text
except Exception as err:
print(f" ! could not read page: {err}")
failed += 1
continue
for img_url in IMG.findall(html):
if img_url in seen:
continue
seen.add(img_url)
dest = os.path.join(OUT, safe_name(img_url))
if os.path.exists(dest):
continue
try:
data = fetch(img_url).content
except Exception as err:
print(f" ! {img_url}: {err}")
failed += 1
continue
with open(dest, "wb") as fh:
fh.write(data)
saved += 1
print(f" saved {os.path.basename(dest)}")
print(f"\nDone. {saved} images in ./{OUT}/ ({len(seen)} found, {failed} failed)")
if __name__ == "__main__":
main()
Running it
Get Python if you haven’t got it
On Windows, the Microsoft Store version is the least painful: search "Python" and install the newest version listed. It sorts out PATH for you, which is the part that usually goes wrong.
Most Linux installs already have it. On a Mac, type python3 in Terminal; if it isn’t there, macOS offers to install it.
Save the script
Copy the code above into a file called download-images.py, anywhere you like. A new empty folder is tidiest, because the images land next to it.
Install the one dependency
In a terminal, run: pip install requests. That’s the only thing it needs beyond the standard library.
On a Mac or Linux the commands are usually pip3 and python3.
Point it at your sitemap
Run: python download-images.py https://www.your-site.com/sitemap.xml. The sitemap URL is the argument, so you don’t need to edit the file at all.
Watch it work
It prints each page as it scans and each image as it saves. When it finishes you have a site-images folder with the lot, and a count of anything that failed.
You can also download the script directly rather than copying it out of the page.
No Python? For a single page, this does the listing part in your browser. Paste the list into any bulk downloader.
Free tool · Image list builder
Get every image URL from a page, no Python needed
Open the page, press Ctrl+U (Cmd+Option+U on a Mac) to view the source, select all, copy, and paste it here. Image sitemaps work too. Nothing leaves your browser.
| Image | Type | Found in |
|---|
Catches img src, srcset (the largest size), picture sources, CSS background images in the page, og:image and image sitemap entries. Images loaded later by JavaScript won’t be in the source, and the script in this guide misses those too.
Things worth knowing
Re-running it’s safe. It skips any file already on disk, so if it dies halfway through a large site — or you just want to catch what has been added since — run it again and it picks up where it left off.
It only finds images the HTML references. Images loaded by JavaScript after the page renders will be missed, as will anything sitting in your Asset Manager that no page actually uses. For a backup of what your site serves, that’s the right behaviour. For a backup of what you have ever uploaded, it isn’t.
Failures are counted, not hidden. A page that times out or an image behind a 403 prints a line and increments the failure count at the end. In my view, silent partial success is the worst outcome for a backup script, so it doesn’t do that.
Which image sizes does the script download?
Every size the page’s HTML lists. The script saves the src image and each width in the srcset as separate files, so one photo can arrive several times. Webflow generates up to seven responsive widths, from 500px to 3200px, for images uploaded directly, so expect several copies of the same picture.
Those widths come from Webflow’s responsive images documentation, which also says variants aren’t created for images in rich text or for background images. Those arrive as a single file.
The script doesn’t read image tags as such. It searches each page’s HTML for any full web address (starting http:// or https://) ending in .jpg, .jpeg, .png, .gif, .webp, .svg or .avif, and saves each address once. In practice that means:
- Every
srcsetwidth is caught, because each is its own address. Keep the largest of each group if you only want one copy. - CSS background images are caught only if they’re in the page’s HTML, in a
styleattribute or a style block. Backgrounds defined in a separate .css file are missed, because the script never downloads stylesheets. - Social sharing images are caught too, since the
og:imageaddress sits in the page’s head. - Image paths written without the domain are missed. A tag pointing at /images/photo.jpg won’t match; only full addresses do. Webflow serves its images from full addresses, so this mostly matters on other platforms.
- Anything after the file extension is dropped. An address ending photo.jpg?w=800 is fetched as photo.jpg, so on sites that resize images through the address you get the version the server sends by default.
- Filenames use the path, not the domain. Two images with the same path on two different domains would share a name, and the second is skipped.
How do you download all images from any website?
Point the same script at the site’s XML sitemap. Nothing in it’s Webflow-specific: it works on WordPress, Shopify, Squarespace, Ghost or a static build, because all it needs is a sitemap and pages anyone can open. To find the sitemap, open the site’s robots.txt and look for a line starting “Sitemap:”.
If robots.txt doesn’t list one, try /sitemap.xml on the domain. The script handles a sitemap index, the sitemap-of-sitemaps many CMSs generate on larger sites, so give it whichever address you find and it will follow the rest.
With no sitemap at all, the script has nothing to walk. For a handful of pages, I’d say the image list builder above is quicker. Webflow is where I needed it first, because it has no download-all button of its own.
On a site that isn’t yours, check its robots.txt and terms first, and see the FAQ below.
What should you do with the images afterwards?
Sort the folder by file size and start with the biggest. A single uncompressed hero image can outweigh everything else on a page, and the download puts every image the site serves in one folder, so the worst offenders show up in seconds instead of page by page.
On a Mac or Linux, ls -lhS site-images | head -20 lists the twenty largest files. On Windows, open the folder in File Explorer, switch to Details view and sort by Size.
I’d ignore the smaller srcset copies; the largest version of each image is the one to fix at source.
Each filename keeps the image’s URL path, so you can find the original in the Asset Manager or the CMS item, compress or resize it, and upload the replacement. Then run the pages that use it through PageSpeed Insights and check Largest Contentful Paint.
My plain-English guide to technical SEO covers what those numbers mean and what else is worth checking while you’re there.
Why this is on an SEO site
Because it’s the same job. Migrating a site without losing its images is a migration problem, and migrations are where organic traffic goes to die — usually through some combination of missing assets, broken redirects and pages that quietly stop being indexed.
I have also used this when auditing: pulling every image a site serves is a fast way to find the 4MB hero nobody compressed, which is often the single biggest thing slowing a page down. If that sounds like your site, that’s the technical work I do.
Common questions
Can the script give me a list of image URLs instead of the files?
Not as written. It downloads the files and prints each filename as it saves it. For a list of image URLs from a single page, with no Python, use the image list builder on this page and paste the result into any bulk downloader.
Is scraping my own website allowed?
Your own site, yes. Be sensible about other people's — check their robots.txt and terms, and don't hammer a server you don't own. The script makes one plain request per page with no concurrency, which is gentle, but that’s a courtesy rather than a licence.
Do I need to know Python?
No. You need Python installed and the ability to copy a file and run one command. The whole thing is about sixty seconds of setup if you have Python already.
Want it done on your site?
Send me the address. The free audit is a short, plain-English read on what to fix first.
Get a free SEO audit