Tools and scripts
How to bulk download every image from a Webflow site
Webflow has no 'download all' button for your assets. Here is a Python script that pulls every image from any site with a sitemap, in one go.
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. The Asset Manager lets you browse your images and download them one at a time. On a site with four hundred assets, that is an afternoon of clicking.
Why the obvious workarounds fall short
Code export is the one people suggest first. It needs a Workspace plan, and the ZIP it produces does not reliably include images embedded in CMS rich text fields — which on a content-heavy site is 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.
What you actually want is something that walks the whole site once and takes everything. Your sitemap already lists every page, so that is the obvious thing to walk.
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 have not got it
On Windows, the Microsoft Store version is the least painful — search "Python 3.12" and install. It sorts out PATH for you, which is the part that usually goes wrong.
macOS and most Linux installs already have 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 is the only thing it needs beyond the standard library.
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 do not 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.
Things worth knowing
Re-running it is 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 is the right behaviour. For a backup of what you have ever uploaded, it is not.
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. Silent partial success is the worst outcome for a backup script, so it does not do that.
Why this is on an SEO site
Because it is 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 is the technical work I do.
Common questions
Does this work on sites other than Webflow?
Yes. It reads a standard XML sitemap and pulls image URLs out of each page's HTML, so it works on WordPress, Shopify, Squarespace, Ghost or a static build. Webflow is just the platform where the missing 'download all' button hurts most.
Will it get images inside CMS collections?
Yes, and this is the main reason to use it over Webflow's own code export. Because it reads the rendered HTML of every page in your sitemap, it catches images in rich text fields and collection templates — which the Asset Manager does not always surface clearly.
Does it download the originals or the compressed versions?
The versions Webflow actually serves, from their CDN — so compressed and resized. For a backup or an audit that is normally what you want. If you need the untouched originals you will have to pull those from the Asset Manager by hand.
Is scraping my own website allowed?
Your own site, yes. Be sensible about other people's — check their robots.txt and terms, and do not hammer a server you do not own. The script makes one plain request per page with no concurrency, which is gentle, but that is 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.